DeepSeek-R1 Distilled Model Runs Locally in the Browser with WebGPU
theme: smartblue
From Zero to One: Running DeepSeek-R1 in the Browser, a Deep Dive into On-Device Large Models and React + WebGPU in Practice
Preface
Have you ever encountered these frustrations: calling large model APIs is expensive, transmitting enterprise data over the network poses security risks, or you want to use AI capabilities in an offline environment but don't know where to start? There is actually a more elegant solution — on-device models, which means deploying LLMs directly on the user's device. Today, I will take you step-by-step to build a DeepSeek-R1 distilled inference demo running in the browser, relying only on React + TailwindCSS + Transformers.js. No server is needed throughout the entire process, and data is absolutely secure.
In this hands-on session, every line of comment in the code hides important knowledge points. We will not only implement the functionality but also interpret the technical principles behind these comments one by one: from the "data-driven" thinking of functional components, to Hooks' state and side effects, to the atomic class philosophy of TailwindCSS, and WebGPU hardware acceleration detection. Complete code and troubleshooting experience are all provided. Let's explore the infinite possibilities of "AI + Frontend" together.
🚨 Spoiler Alert: This is only the first part of the entire project! We have currently completed the interface skeleton and state system. The next article will add the model download progress bar at the bottom, making the loading process clear at a glance. Remember to follow so you don't miss the sequel~
1. Why Do We Need On-Device Large Models?
In traditional large model application architectures, we usually call remote LLM services via APIs, such as OpenAI or DeepSeek's cloud interfaces. This approach has two fatal problems:
- High Cost: Billed per token, long-term use is a significant expense for both enterprises and individuals.
- Insecure: All context data is sent to the server with the request, posing a risk of sensitive information leakage.
On-device Models deploy LLMs directly on the user's device, such as mobile phones, in-vehicle systems, or even browsers. Their core advantages:
- Data Privacy Guaranteed: All inference is done locally, and data never leaves the device.
- Offline Availability: Once the model is downloaded, it can be used normally even without a network connection.
- Low Cost: No pay-per-use fees, especially suitable for high-frequency simple tasks.
- Low Latency: Network transmission is skipped, resulting in faster response times.
Of course, on-device models have certain hardware requirements. We usually use open-source small-parameter models (e.g., 1.5B scale). After distillation and optimization, they are sufficient for tasks like translation, summarization, and simple conversations. Running them in the browser can leverage WebGPU hardware acceleration, achieving an extremely lightweight experience of downloading and using anytime, anywhere.
📌 Tip: To play with on-device models locally, you can first use Ollama to experience the joy of one-click deployment of open-source models. It supports Windows/Linux/macOS, and a single command can run models like Llama and Qwen locally. But today, we go a step further, letting the model run directly in a browser tab!
2. Technology Selection: React + TypeScript + TailwindCSS + WebGPU
The frontend of modern large-scale AI projects almost inevitably involves React. Compared to Vue, React has a slightly higher learning curve, but with its flexible functional programming paradigm and vast ecosystem, it has become the preferred choice for large model applications. Especially the combination of functional components + Hooks makes the relationship between data and UI exceptionally clear—you only need to define the state, and React will automatically update the interface for you.
Compared to Vue's SFC (template + script + style sandwich structure), a React component is essentially a function that returns JSX. JS logic and HTML structure are naturally integrated, while CSS is injected through independent style files or atomic tools like TailwindCSS.
Technology Stack Overview
- React 18 + TypeScript: Strong type constraints + functional components + Hooks (
useState,useEffect, etc.) - Vite: Blazing-fast build tool with native JSX/TSX support
- TailwindCSS: Atomic CSS framework, almost eliminating the need to write custom CSS
- ESLint: Unified code style, essential for large team collaboration
- Transformers.js + ONNX Runtime Web: Load and run distilled large models in the browser
- WebGPU: Leverage local GPU hardware acceleration for inference
3. Project Initialization: Starting with a Single Command
Open the terminal and use the Vite template to quickly create a React + TypeScript project:
npm init vite@latest deepseek-webgpu -- --template react-ts
cd deepseek-webgpu
npm install
The generated project is already configured with ESLint to ensure consistent code style—this is especially important for team projects or open-source projects. For example, it enforces whether to use single or double quotes, and whether indentation is 2 or 4 spaces, bidding farewell to chaotic code formatting.
4. Introducing TailwindCSS: Goodbye to the Inefficiency of Traditional CSS
Traditional CSS requires defining selectors, writing property values, and then carefully handling specificity and style conflicts. It's like programming directly in binary—inefficient and error-prone.
TailwindCSS brings an "atomic" mindset: it provides thousands of semantic class names (like flex, text-center, bg-white). You simply combine these class names on HTML tags to quickly build complex interfaces.
Install and configure TailwindCSS (based on the Vite plugin):
npm install -D tailwindcss @tailwindcss/vite
Modify vite.config.ts:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
})
Add the following at the top of the entry CSS file (src/index.css):
@import 'tailwindcss';
Now you can directly use atomic classes like className="flex flex-col items-center" in your components. Note that in JSX, we use className instead of class, because class is a JavaScript keyword (used for declaring classes), so React chose className as a substitute. This is a small detail of JSX syntax.
5. React Functional Components: Building UIs Like Building Blocks
React breaks down the interface into independent "building blocks"—components. Each component is essentially a function that returns JSX. You can write any JavaScript logic inside the function, and finally return a piece of JSX code that looks like HTML.
Understanding JSX: JSX is a combination of JavaScript and XML. It allows us to write HTML tags directly in JS files, which are compiled into native DOM operations. This is one of React's proudest features, making UI expression intuitive and efficient.
Let's see how the entry file src/main.tsx mounts the component:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
<App /> is our top-level component, which will be rendered into the div with the ID root on the page. The entire application is like a tree composed of components, nested layer by layer.
6. Step-by-Step Breakdown of the Core Component App.tsx (In-Depth Comment Analysis Version)
Below is our protagonist—App.tsx. I will guide you through the key knowledge points line by line, combining class notes. Pay special attention to the comments in the code; they are more important than the code itself—each comment is a distillation of design thinking, API principles, and engineering practices.
1. Understanding the Essence of Modern Frontend Frameworks
The three core concepts of modern frontend frameworks: Componentization, Reactivity, and Data Binding.
Componentization: Whether React or Vue, the interface is split into independent, reusable components. Each component is like a black box, encapsulating its own structure (HTML), style (CSS), and interaction logic (JS). In React, this "black box" is a function; in Vue, it is a .vue single file. This encapsulation allows us to combine complex pages like building blocks, greatly improving code maintainability and reusability.
Reactivity: This is the most fundamental difference between modern frameworks and the jQuery era. We no longer need to manually find DOM nodes and modify their properties. Instead, we declaratively describe "what the UI should look like when the data is X." The framework automatically tracks data changes and efficiently updates the corresponding DOM nodes. React triggers re-renders through useState, while Vue implements it through Proxy interception of ref and reactive. The reactivity mechanism allows developers to focus on data and business logic, without getting bogged down in tedious DOM operations.
Data Binding: Data binding is the bridge connecting the interface and data. In JSX, we use {} for one-way data binding: data flows to the view. For example, <p>{count}</p>, when count changes, the content of <p> updates automatically. Vue supports two-way binding (v-model), but React advocates for one-way data flow, which makes it easier to track state changes and avoid data chaos. The concept of data-driven views eliminates the need for DOM programming -> data state (reactive, modify the state, and the interface follows).
The "function encapsulation characteristic" emphasizes that a React component is a function, possessing the natural advantages of functions: receiving parameters, returning results, composable, and reusable. This abstraction method, with functions as the smallest unit, makes the process of building UIs as natural as writing ordinary JavaScript logic.
2. Introducing Hooks: The Essence of Functional Thinking
import {
useState , // react functional thinking hooks each function has a hooks state, starting with use
useEffect , // lifecycle hook function, executed when the component mounts
} from 'react'
useStateComment Interpretation: React's functional components are inherently stateless, but through theuseStateHook, we can "mount" reactive state onto the component. It's like attaching a hook to a function, allowing it to remember data. Any function whose name starts withuseis a React Hook, and they can only be called at the top level of a component function.useEffectComment Interpretation:useEffectis known as the side effect Hook. It can simulate lifecycle methods in class components. When the component mounts (first inserted into the DOM), when the state updates, or even when it is destroyed, the logic inside can be triggered. The comment on the second line, "executed when the component mounts," is just a common use case; in reality, its trigger timing is determined by the dependency array.
3. Structural Comments of a Functional Component: A Code Skeleton with Clear Responsibilities
The standard structure of a functional component:
- Hooks at the top: Call various
useState,useEffect, etc., at the top of the component to declare state and side effects. - Logic in the middle: Process data, events, conditional judgments, etc.
- JSX at the end: Return a JSX template describing the UI structure.
A React component is just such a pure function: input data, output interface—clean and predictable.
4. The Philosophy of State Design: UI = f(state)
const [status, setStatus] = useState<string | null>(null); // reactive data state
This comment is extremely important; it reveals the core concept of React—data-driven views.
- Traditional DOM Programming vs. Reactivity: Previously, we needed to manually select DOM elements and modify their
innerTextorclassName. Now, we just need to define the state (likestatus) and describe what the interface should look like under each state. React automatically completes the DOM updates. - State Enumeration:
null(not yet started),loading(loading),ready(ready),error(failed)... These states are like the masks in Sichuan Opera face-changing. When the data changes, the interface switches instantly, and what the user sees is a "snapshot" of the current moment. - Type Constraint:
useState<string | null>(null)specifies through generics that the state can only be a string ornull. TypeScript will strictly enforce this for us.
5. Error State and Loading Progress: Making Feedback More Refined
const [ error, setError] = useState<Error | null>(null); // error object data state
const [ loadingMessage, setLoadingMessage ] = useState(""); // loading state
const [ progressItem, setProgressItem ] = useState([{
file:'model.onnx',
progress:0,
total:66666666,
}]); // progress state
errorstores a JavaScriptErrorobject, from whichmessageandstackcan be extracted for precise error prompts.loadingMessageprovides dynamic loading text, such as "Downloading model file...", to alleviate waiting anxiety.progressItemtracks the download progress of the model file (likemodel.onnxin ONNX format).progressandtotalrepresent the downloaded and total bytes, respectively. Key point here:progressItemis reserved for the download progress bar in the next article! Currently, it is not rendered in the interface, but the state is already in place. In the next article, we will add a cool caching progress bar at the bottom of the page, making the loading process clear at a glance.
6. WebGPU Capability Detection: The Double Negation Trick
// Browser navigation bar whether WebGPU is supported
// Important feature of modern browsers
// ! negates navigator.gpu when unsupported it is undefined
// !! negates again, definitely becomes true | false
// Double negation equals affirmation
const IS_WEBGPU_AVAILABLE = !!navigator.gpu;
This comment vividly explains the double negation operation:
navigator.gpuis the entry object for WebGPU. If the browser does not support it, it isundefined.- One
!converts it to a boolean and negates it:undefined→true,object→false. - Two
!negates it again, yielding the original boolean value:undefined→false,object→true. - So
!!is equivalent toBoolean()type coercion, commonly used to convert a variable that "might be falsy" into a definitetrue/false. Here, gettingtruemeans the browser supports WebGPU, and subsequent hardware-accelerated inference can proceed.
7. useEffect's Mounting and Side Effects
// useEffect used, executes when status state changes
// Component lifecycle side effects
// Side effects what to do after the component mounts
useEffect(() => {
console.log('Component has mounted')
// setTimeout(() => {
// setStatus('ready')
// }, 2000)
}, [status])
- Side Effect refers to operations that are unrelated to component rendering but must be executed, such as data requests, subscriptions, manual DOM modifications, etc. The
console.loghere is a simple side effect. - Dependency Array
[status]: Indicates that the function insideuseEffectwill only re-execute when the value ofstatuschanges. If the dependency array is empty[], it only executes once when the component first mounts, similar tocomponentDidMount. - The comment mentions using
setTimeoutto simulate a state change fromnulltoready, which is exactly the core flow we need to implement when loading the model later.
8. Execution of Component Functions and State Changes
console.log('Component function executed')
// js script data logic interaction
// count data state
// modify count setCount
const [count, setCount] = useState(0) // reactive state ref
A count state not used in the UI is deliberately introduced here to demonstrate React's re-rendering mechanism. Whenever the state is changed via setCount, the entire App function is called again, and the console prints "Component function executed" once more. But note, the value of count is retained and incremented by React, because useState maintains all component states through a linked list at the底层 (underlying level). This experiment proves that although a functional component is an ordinary JavaScript function, it gains the ability to "remember" within React's runtime environment.
9. Conditional Rendering and Layout Comments in JSX
return (
// Atomic classes, combine them
IS_WEBGPU_AVAILABLE ? (
<div className="flex flex-col h-screen mx-auto items-center justify-center text-gray-800 bg-white">
Conditional Rendering: The ternary operator
IS_WEBGPU_AVAILABLE ? (...) : (...)is the most direct way of conditional rendering, displaying different complete interfaces based on WebGPU support. This approach ensures that the UI description for each branch is complete and easy to read.Atomic Class Combination: The string of classes
flex flex-col h-screen mx-auto items-center justify-centerimplements a flexible vertically centered layout:flexenables Flex layoutflex-colsets the main axis to verticalh-screenmakes the height fill the entire viewportmx-autosets horizontal auto margins (centering)items-centercenters on the cross axis (horizontally)justify-centercenters on the main axis (vertically)
Note JSX Comment Format: In JSX, comments must be wrapped in
{/* comment content */}, and ordinary//or/* */cannot be used, because JSX is ultimately compiled into JavaScript calls, and ordinary comments would be treated as text nodes.
10. Title Area Comments: Model Information and Community Links
{/* Title Area */}
<div className="flex flex-col items-center mb-4 max-w-[400px] text-center">
{/* Distilled from Qwen */}
<h1 className="text-4xl font-bold mb-1">DeepSeek-R1 WebGPU</h1>
<h2 className="font-semibold">
A next generation reasoning model that runs locally in your browser with WebGPU acceleration.
</h2>
</div>
- Distillation: The comment points out that the model is distilled from the Qwen architecture. Knowledge distillation is a model compression technique that uses a large "teacher model" to teach a smaller "student model," allowing the small model to maintain accuracy close to the large model while significantly reducing the number of parameters, making it very suitable for on-device deployment.
11. Model Introduction Area: The Golden Triangle of the Open-Source Ecosystem
<a
href="https://huggingface.co/onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX"
target="_blank"
rel="noreferrer"
className="font-medium underline"
>
{/* DeepSeek-R1's 1.5 billion parameter distilled version, using Qwen architecture, suitable for local lightweight inference.
Distilled Qwen Reasoning
HuggingFace the world's largest open-source model community
*/}
DeepSeek-R1-Distill-Qwen-1.5B
</a>
, a 1.5B parameter reasoning LLM optimized for in-browser
inference. Everything runs entirely in your browser with{" "}
{
// transformers is a js library provided by huggingface for loading and inferencing models
}
<a
href="https://huggingface.co/docs/transformers.js"
target="_blank"
rel="noreferrer"
className="underline"
>
🤗 Transformers.js
</a>{" "}
{/* Open Neural Network Exchange */}
and ONNX Runtime Web, meaning no data is sent to a server. Once
loaded, it can even be used offline. The source code for the demo
is available on{" "}
This section of comments is extremely rich in information, serving as the "ecosystem manual" for the entire demo:
- DeepSeek-R1-Distill-Qwen-1.5B: A 1.5 billion parameter reasoning model, optimized for local lightweight inference, especially adept at complex reasoning tasks.
- HuggingFace 🤗: The world's largest open-source model community, providing a range of tools such as model hosting, version management, and inference libraries. It is affectionately called "Hugging Face" by developers. The model we use is hosted under its
onnx-communityorganization. - Transformers.js: An official JavaScript inference library from HuggingFace, enabling Transformer models to run in the browser without a backend. It internally calls ONNX Runtime Web or directly uses WebGPU.
- ONNX Runtime Web: A runtime for the Open Neural Network Exchange format, supporting the WebGPU backend to further accelerate model inference. ONNX is a universal model storage format that allows trained models to run on different frameworks and hardware.
- Privacy Promise: All computations are performed locally in the browser, no data is sent to any server, and it can even be used offline. This is the greatest charm of on-device models.
12. Error Interface: Reactive Error Prompt
{/* Error interface state, reactive */}
{
error && (
<div className="text-red-500 mb-2 text-center">
<p className="mb-1">
Unable to load model due to the following error:
</p>
<p className="text-sm">{error.message}</p>
</div>
)}
- Conditional rendering is implemented using
error && (...): the error prompt card is only displayed whenerroris notnull. Becauseerroris a reactive state, once it is set, the component immediately re-renders, and the user sees the error information instantly. This "change-and-show" experience is difficult to achieve elegantly with traditional DOM manipulation.
7. Model Loading Process: Transformers.js and ONNX Runtime Linkage
In useEffect or other lifecycles, we will actually call Transformers.js's pipeline API to download resources like the model and tokenizer. Simultaneously, we use setProgressItem to update the download progress and setStatus to switch states. Once the model is ready, status becomes ready, and the user can start a conversation.
In the current UI phase, we haven't implemented the actual inference logic yet, but all states and the interface skeleton are fully built. Later, we just need to call the model pipeline and update loadingMessage or the conversation history when a button is clicked or text is entered.
8. Summary and Outlook
Through this project, we have connected the core concepts of on-device large models, React functional components and Hooks, TailwindCSS atomic styling, and the practical application of WebGPU hardware acceleration. But more importantly, we savored the comments in the code line by line—they are the condensation of thought and the annotation of knowledge.
What have you learned?
- The value and applicable scenarios of on-device models; data security is always the top priority.
- React functional component = Hooks (state + side effects) + Logic + JSX, simple yet powerful.
- Commenting is a form of engineering literacy: good comments explain "why it's done this way," not just "what was done."
- Atomic classes and reactive thinking make UI development as fluid as writing natural language.
- WebGPU opens a new door for high-performance AI inference in the browser.
In the future, on-device models will shine in fields like IoT, autonomous driving, and industrial edge computing. The browser, as the most universal terminal, will inevitably become an important distribution point for AI capabilities. I hope this hands-on article, starting from scratch, can help you push open this door and build smarter applications with code and comments together.
9. Next Episode Preview: The Progress Bar is Coming! 🚀
So far, our DeepSeek-R1 WebGPU project has set up a complete UI skeleton and state system. The interface displays the title, model introduction links, and can respond to WebGPU support status and error states—only the model download progress bar at the bottom is missing.
Don't worry, this is exactly the highlight of our next article! By then, we will:
- Add an elegant progress bar component at the bottom of the page
- Use the
progressItemstate to update the download percentage in real-time - Cooperate with
loadingMessageto display dynamic prompt text - Transform the entire loading experience from "dry waiting" to a "visualized" enjoyment
Hey, thanks for reading this far! 🎉 If this article sparked new ideas about on-device AI and React for you, don't be stingy with your likes and bookmarks, and feel free to forward it to friends who are also tinkering with large model frontends~ If you have any wild on-device AI ideas or pitfalls you've encountered, the comment section is your stage. Let's exchange ideas and progress together! Don't forget to follow; the next progress bar hands-on is already on its way. See you there! 👋