跪拜 Guibai
← Back to the summary

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.

Running a large model in the browser? On-device AI hands-on cover image

🚨 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:

On-device Models deploy LLMs directly on the user's device, such as mobile phones, in-vehicle systems, or even browsers. Their core advantages:

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


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).

Three Core Concepts Diagram

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'

3. Structural Comments of a Functional Component: A Code Skeleton with Clear Responsibilities

The standard structure of a functional component:

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.

Data Driven

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

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:

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])

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">

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>
Knowledge Distillation Concept Diagram.png

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"
            >
              🤗&nbsp;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:

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>
          )}

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.

Browser-side Transformers.js ONNX Local Inference Architecture Diagram

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?

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:


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! 👋