跪拜 Guibai
← Back to the summary

A 1.5B-Parameter Reasoning Model Running Locally in Your Browser with WebGPU

Top-Level Conclusion

With just four core files — index.html, main.tsx, App.tsx, index.css — we built a WebGPU application that runs a 1.5-billion-parameter inference model locally in the browser. The essential significance of this is: the browser is becoming a first-party runtime for AI inference, and the modern frontend engineering stack (Vite + React + TypeScript + TailwindCSS) happens to provide the lowest-barrier implementation path for this.

Below, we break it down layer by layer from the top down.


1. What is this project doing? — The "supercharged" positioning in the readme.md opening

This project's positioning was never a "practice demo" from the start, but rather the page on your resume that can really deliver. The skill points it covers are all the content we will unpack below.

1.1 On-device models: Why not just use an API?

I gave the most direct comparison:

Unlike calling the OpenAI/DeepSeek API, the LLM is remote and not co-located with the calling client.

Two structural pain points follow:

Expensive. Remote APIs charge per token, and you pay for every inference. When an application faces a massive user base, costs become uncontrollable.

Insecure. I's phrasing is: "context is sent along with the request to" — the user's conversation history, document content, code snippets, every HTTP request leaves your device. No matter what the service provider promises, the moment data leaves your hands, control no longer belongs to you.

The only way out is — on-device models. In fact:

Ollama local open-source model deployment, on the user side, on-device models. Mobile side, automotive side, Agent task partitioning. Open-source small-parameter models can accomplish these tasks. Browser side, download anytime, use anytime. webGPU.

This passage clarifies the complete landscape of on-device models: Ollama solves local deployment on the desktop → mobile and automotive sides run small-parameter models for Agent tasks → Browser + WebGPU is the lightest form — open a webpage, the model downloads locally, GPU inference, ready to use anytime, gone when you leave.

The core proposition of this project is thus established: Use a 1.5-billion-parameter distilled model (DeepSeek-R1-Distill-Qwen-1.5B-ONNX) to achieve local inference in the browser with the help of WebGPU.


2. Technology Selection: Why do these four things come together?

2.1 React + TypeScript: The first choice for large projects in the AI era

readme.md's judgment is very clear:

React + TS — the preferred frontend technology for large projects in the AI era.

Why React and not Vue? I gave three progressively deeper reasons:

First, "React is harder to get started with than Vue" but has a higher ceiling. Vue's template/script/style sandwich structure is beginner-friendly — in a .vue file, template, logic, and styles each occupy their own block. I's assessment is "Vue is easy to get started with." But React's thinking is different:

Encapsulate a component (function), the function is the component. The function returning HTML is the component. The JS part before the function returns, CSS? Import styles.

HTML, CSS, and JS are no longer separated by "type" into files, but encapsulated by "functionality" within a single function. I calls this "building pages the way you build with blocks" — each block (component) is a functional unit composed of a set of HTML, CSS, and JS.

Second, "AI training code is predominantly React." The open-source community's training data and code templates are mainly React, which means AI-assisted coding tools like Copilot and Cursor have better completion quality in React projects.

Third, "Large projects." React's reactive data binding is paradigm-level — data changes, the interface updates automatically. Developers don't care about appendChild or innerHTML, they only manage state.

2.2 Project Initialization: The constraining significance of ESLint

I emphasized the initialization steps in the "New Project" section:

react + ts + eslint (code constraints, essential for large companies, consistent code style) '' "" ; eslint is responsible for constraining code style

'' or ""? Add ; or not? These problems are infinitely magnified in multi-person collaboration. If code style is inconsistent, code review cannot be discussed — the reviewer's attention is consumed by formatting noise, and logic problems are instead concealed. The ultimate value of ESLint is making the entire team's code look like it was written by a single person.

tsconfig.json adopts a layered strategy with project references — tsconfig.app.json and tsconfig.node.json separate frontend code and build configuration. For an application involving the WebGPU API (navigator.gpu), model loading state machines, and multiple types of error objects, type safety is not optional, it's a hard requirement.

2.3 Vite: HMR decouples the heavy from the light

For an application that needs to load ONNX model files that can be several GB in size, the development experience directly determines iteration efficiency. Vite's HMR (Hot Module Replacement) means modifying any single line of UI in App.tsx gives millisecond-level feedback in the browser — no need to reload the page, no need to re-fetch the model. Model loading is heavy, UI iteration is light, and HMR lets you completely decouple the two.

vite.config.ts is less than ten lines:

export default defineConfig({
  plugins: [react(), tailwindcss()],
})

Two plugins, zero-config startup. package.json's scriptsdev, build, lint, preview — four commands cover the full lifecycle.


3. TailwindCSS: The paradigm shift from "writing CSS" to "writing class names"

3.1 Installation and Core Philosophy

readme.md directly gives the installation command:

npm install tailwindcss @tailwindcss/vite

Then it gives TailwindCSS's positioning:

Almost no longer need to write CSS, atomic CSS classes.

To understand the weight of this sentence, you need to first understand why traditional CSS is inefficient. In the traditional model, your mental burden is: think of a class name → write a selector → write rules (key: value;) → reference it in HTML → jump back and forth between two files. readme.md's analogy is very precise:

If previous CSS selectors, rules (key:value;) were too low-level, too inefficient → binary. Writing class names is better.

The "binary" analogy is the key to understanding TailwindCSS. Handwriting display: flex is like handwriting 01100100 — you are describing intent with low-level instructions, rather than directly expressing intent. What TailwindCSS does is encapsulate CSS rules into semantic atomic class names: flex = display: flex, text-center = text-align: center. You no longer write low-level rules, but compose class names.

3.2 How TailwindCSS Works

readme.md clearly explains how it works:

Not native CSS. An atomic class CSS framework, providing a bunch of CSS class names (atomic classes). No need to write CSS anymore, selectors and CSS rules are too inefficient. The vite plugin can be used, it extracts the styles of the class names we declare and adds them to the code.

The key chain: You declare class names in JSX's className → The Vite plugin scans the source code at build time → Extracts the corresponding styles → Injects them into the final CSS output. You declare what class names, and the build artifact contains exactly those styles, no more, no less.

readme.md further points out the advantages of this system:

Atomic class names (English words), simple, very good semantics, especially suitable for natural semantic programming.

Reading flex items-center justify-between allows you to reconstruct the layout in your mind. LLMs understand semantic naming far better than custom CSS — so I asserts:

tailwindcss has become a fundamental component of vibe UI.

3.3 Why className and not class?

readme.md gives a clear answer:

className? The class name class. class is a keyword for class names (OOP) in js. Inside a function, you write JSX, so you cannot use class, understand it as you are declaring a js class. className does not have this problem.

JSX is essentially JavaScript — the <div> tags you write inside () are ultimately compiled into React.createElement('div', ...) calls. Writing class="container" in JS makes the compiler think you are declaring a JavaScript class. className is React's JSX mapping of the HTML class attribute — it bypasses the keyword conflict, and the semantics are completely equivalent.

3.4 index.css — A Design System in One Line of Code

@import "tailwindcss";

Just one line. TailwindCSS v4's zero-configuration design philosophy — compared to v3 which required configuring content paths, theme extensions, and plugin registration in tailwind.config.js, v4's @import "tailwindcss" achieves "import is configuration." The CSS file itself is the single configuration entry point.

The significance of this for AI application UIs: The interface of an AI Demo changes extremely fast — today add a loading progress bar, tomorrow change the error prompt style. Under traditional solutions, you have to switch between the style file and the component every time. TailwindCSS inlines all style decisions within JSX's classNamewhen you look at the component code, you simultaneously see its appearance and behavior.


4. JSX: Writing HTML inside JavaScript

Before entering App.tsx, we must first clarify JSX. readme.md gives a complete definition:

JSX is React-specific syntax (template). It allows you to directly write HTML tags in JS code, after compilation — JSX: JavaScript's HTML-like syntactic sugar, used for intuitively writing page structures in React, compiled into native JS to create DOM.

One of React's proudest major features, very convenient for expressing UI interfaces. javascript with xml. <div></div> is HTML's unique XML.

JSX = JavaScript + XML. <div></div> is XML tag syntax, and now it can appear inside the function body of a .tsx file:

2f28a21436cc158b4b14e80cb31c70ee.png

function App() {
  return (
    <div className="container">
      <h1>Hello</h1>
    </div>
  )
}

After compilation, it becomes native JS DOM creation operations. The significance of JSX is not writing less code, but that page structure and business logic are in the same context — no need to jump between .html and .js. For scenarios like WebGPU that require extensive state management, JSX's conditional rendering {error && <ErrorUI />} makes the mapping from state to UI exceptionally concise — you write "render this section when error exists," not "find that div, change its display property."


5. Application Skeleton: The Initialization Logic of main.tsx

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

Five lines of code, three key decisions:

First, the enabling of StrictMode. React 19's StrictMode double-invokes component functions and effects in the development environment, deliberately exposing potential side-effect problems. Model loading is a typical side-effect-intensive operation (asynchronous requests, GPU device acquisition, memory allocation), and StrictMode helps developers discover impure logic in state management early on.

Second, the timing of CSS import. import './index.css' is placed before the App component import. The loading order of CSS affects cascade priority — TailwindCSS's utility classes need to be injected first as the base layer.

Third, the non-null assertion of createRoot + getElementById('root')!. ! is trust in the "HTML structure contract" — the root node is a fixed part of the index.html template, and there is no scenario where it is dynamically removed at runtime. This is a well-founded assertion in TypeScript: not blindly bypassing type checking, but a reasonable inference based on invariants.


6. Core Engine: The Six-Layer Design Deconstruction of App.tsx

This is the soul of the entire project. I will deconstruct App.tsx from the outside in into six layers, each focusing on an independent concern.

Layer 1: Componentization Thinking — React's Mental Model

The comments at the beginning of the file condense React's core values from readme.md into four lines:

// Modern frontend development framework
// .vue->tsx componentization typescript+ jsx
// Reactive
// Data binding
// Function encapsulation feature: encapsulate a component's html, css, js into one component

This is exactly what readme.md meant by "building pages the way you build with blocks." The four comments correspond completely to readme.md:

Layer 2: State System — The Application's Data Skeleton

const [status, setStatus] = useState(null);
const [error, setError] = useState<Error | null>(null);
const [loadingMessage, setLoadingMessage] = useState('出错了');
const [progress, setProgressItem] = useState([{
  file: 'model.onnx',
  progress: 0,
  total: 34353543453
}])

Four useState calls form a complete state machine:

State Variable Type Role Initial Value Meaning
status null | string Application lifecycle null = Uninitialized
error Error | null Exception capture null = No error
loadingMessage string User prompt text '出错了' as the default fallback
progress Array<{file, progress, total}> Model download progress Pre-filled single-file tracking for model.onnx

Design ingenuity here:

Layer 3: WebGPU Capability Detection — The Application's Technical Feasibility Watershed

3a7ad90d75e14cffa11e747b4437b8bd.png

const IS_WEBGPU_AVAILABLE = !!(navigator as any).gpu;

This single line is the technical feasibility watershed of the entire project. Let's deconstruct its three layers of meaning:

  1. (navigator as any): TypeScript type assertion. navigator.gpu has not yet entered TypeScript's standard DOM type definitions. As the WebGPU standard lands, this assertion will eventually be removed.
  2. .gpu: The entry object for the WebGPU API. If the browser supports WebGPU, it returns a GPU object; otherwise, it returns undefined.
  3. !! double negation operator: Forces GPU object | undefined into true | false. The most concise truthy normalization pattern in JavaScript.

This boolean value determines the fork of the entire subsequent rendering path. Browsers that do not support WebGPU directly see a degradation prompt, while browsers that support WebGPU enter the full application interface. This is an elegant progressive enhancement strategy — unsupported browsers don't crash, the functionality is simply unavailable.

This is precisely the first technical hurdle of the on-device model vision in readme.md: The browser supports WebGPU, inference can run on the GPU; if not, everything is off the table. This line of code is the guardian of the entire project's "feasibility."

Layer 4: Lifecycle Management — Side Effects and State Flow

image.png

useEffect(() => {
  console.log('组件已经挂载完成')
  setStatus('ready');
}, [])

The second argument to useEffect is an empty array [], meaning this effect executes only once after the component first mounts.

Execution sequence:

  1. The component function executes for the first time → JSX renders to the DOM
  2. DOM mount completes → useEffect callback triggers
  3. setStatus('ready') → triggers a re-render, UI enters the ready state

console.log('组件函数执行') prints on every render in the function body, while console.log('组件已经挂载完成') prints only once after mounting. The contrast between the two clearly demonstrates that React's "rendering" and "mounting" are two distinct phases.

The commented-out setTimeout code in the comments:

// setTimeout(()=>{
//   setStatus('loading');
// },2000)

Reveals the developer's evolutionary thinking — first manually delay to simulate the loading state to verify UI performance, and later, when connecting the real model loading logic, directly replace this setTimeout with the real asynchronous callback. This is a typical "bottom-up" development strategy: first verify the UI state switching logic, then connect the real data source.

Layer 5: Conditional Rendering — The Forked Architecture of Two UIs

1c2f27171b5bb2eb3adef0b8ae95dcbd.png

return (
  IS_WEBGPU_AVAILABLE ? (
    <div className="flex flex-col h-screen mx-auto items-center justify-end text-gray-800 items-center">
      {/* Full application interface */}
    </div>
  ) : (
    <div>您的浏览器还不支持WebGPU</div>
  )
)

The top-level ternary expression implements two mutually exclusive rendering paths. This is not simple "if-else," but the core pattern of React's declarative rendering — data (IS_WEBGPU_AVAILABLE) drives the interface, with no manual DOM operations.

TailwindCSS class analysis of the full application interface:

flex flex-col          → Flex layout, main axis vertical
h-screen               → Height = 100vh (fills the entire viewport)
mx-auto                → Horizontal centering
items-center           → Child elements centered on cross axis
justify-end            → Child elements aligned to end of main axis (bottom)
text-gray-800          → Text color

Here items-center appears twice — both the outer container and some inner element have it set, which is style redundancy. The extra items-center at the end won't error but also won't take effect (already overridden by the outer layer), reflecting traces left from style debugging during development. In a real project, this redundancy should be cleaned up.

Layer 6: Content Area — Model Information Architecture and Error Handling

Title Layer:

<h1 className="text-4xl font-bold mb-1">Deepseek R1 WebGPU Demo</h1>
<h1 className="text-2xl font-bold mb-1">Deepseek R1 WebGPU模型</h1>

Two h1 tags are an anti-pattern in semantic HTML — a page should only have one h1. But the font size contrast between text-4xl and text-2xl achieves a clear visual hierarchy — pragmatism takes priority over specification.

Model Source Layer:

<a href="https://huggingface.co/onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX">
  DeepSeek-R1-Distill-Qwen-1.5B
</a>

This link is precisely annotated by comments:

Distilled Qwen Reasoning inference model HuggingFace, the world's largest open-source model community

The complete technology stack chain is: DeepSeek R1 (original large model) → Distillation (Distill) → Qwen 1.5B architecture (lightweight) → ONNX format export (cross-platform) → Transformers.js loading (browser-side) → ONNX Runtime Web inference (WebGPU acceleration)

Each step is a compromise and optimization "to make way for browser inference." This is precisely the practical path of on-device models in readme.md — starting from a large model, through distillation, quantization, and format conversion, finally squeezed into the browser's sandbox. Each step sacrifices capability for accessibility, but the final result is: AI inference that can be launched with a single URL.

Error Handling Layer:

{error && (
  <div className="text-red-500 text-center mb-2">
    <p className="mb-1">Unable to load the model due to the following error:</p>
    <p className="text-sm">{error.message}</p>
  </div>
)}

A classic pattern of React conditional rendering — error && (...) short-circuit evaluation. When error is null, the entire block does not render; when error has a value, the red error message is displayed. text-red-500, text-center, mb-2 three utility classes complete the entire style expression of the error prompt, without a single line of custom CSS — this is the power of TailwindCSS's "just write class names" in a real scenario.


7. Architecture Panorama: How Four Files Form a Complete System

Returning to the top of the pyramid, the division of labor among the four files is as follows:

index.html          →  Runtime container (provides root mount point + module entry)
    │
main.tsx            →  Application bootstrap layer (CSS injection + React initialization + StrictMode)
    │
index.css           →  Design system layer (TailwindCSS atomic class system)
    │
App.tsx             →  Business logic layer (state management + WebGPU detection + UI rendering + error handling)

Key insight: Each file does only one thing, and the dependencies between them are unidirectional.

This unidirectional dependency architecture means: you can replace App.tsx with any other application, and main.tsx and index.html require no changes at all. Separation of concerns is not a slogan, but is embodied in the physical boundaries of file responsibilities.


8. Extended Thinking: The Technical Boundaries and Future of this Demo

This project is currently at the stage of a WebGPU capability showcase + UI skeleton construction. The code has already reserved expansion interfaces:

  1. progress state array → Later connect to real model download progress callbacks
  2. The commented-out setTimeout → Replace with Transformers.js's asynchronous pipeline
  3. error state → Capture various exception scenarios like model loading failure, GPU memory shortage
  4. IS_WEBGPU_AVAILABLE → In the future, can be expanded to a multi-level fallback strategy: WebGPU → WebNN → WASM

When these interfaces are filled, this four-file project will evolve from a Demo into a complete browser-side AI inference application — all computation happens on the user's GPU, and all data never leaves the browser.

Returning to that sentence at the beginning of readme.md"The supercharged project on your resume." Its "supercharged" nature doesn't lie in the number of lines of code, but in that it touches a truly cutting-edge intersection: using the modern frontend toolchain of React + Vite + TailwindCSS to squeeze a 1.5-billion-parameter AI model into the browser's WebGPU runtime. Expensive and insecure are structural problems of remote APIs. The only way out is to move the model onto the user's device — Ollama solved the desktop side, and WebGPU + browser solves the lightest touch: one link, zero installation, ready to use immediately.

The browser is becoming the next model inference runtime. And React + Vite + WebGPU is the most concise ticket to this future.