跪拜 Guibai
← Back to the summary

WebGPU in the Browser Lets Frontend Devs Run AI Models with Zero Backend

🎬 Synopsis: An ordinary frontend developer suddenly discovers that the browser can directly call the GPU to run AI models, no backend required. The following is their entire journey from "Huh?" to "Holy crap!"


Opening: The Anxiety of a Frontend Developer

It's 2025, and the frontend world is cutthroat.

You just learned Webpack, and Vite is already on version 8. You just figured out Vue 3, and React is at 19. You've finally managed to write full-stack, and your boss says, "We're building an AI product."

You search for tutorials—and wow, Python, PyTorch, CUDA, graphics cards, servers... it's all stuff you need to learn from scratch.

Wait. What if I told you: using only JavaScript, you can run large models right in the browser? You don't need to learn Python, don't need to buy a graphics card, don't need to set up a server—do you believe it?

I didn't believe it at first either. Not until I ran DeepSeek-R1 inside a Chrome browser.

This article walks you through the entire process. Zero Python, zero backend, pure frontend.


Chapter 1: When Did the Browser Secretly Get So Powerful?

What Is WebGPU?

Previously, the only channel for a browser to use the GPU was WebGL. That's a relic from 2011, originally designed for web 3D games, and its performance and features can't keep up with the demands of AI inference.

In 2023, Chrome 113 was released, bringing WebGPU—a brand-new browser GPU API that directly rivals native graphics interfaces like Vulkan, Metal, and DX12.

An analogy:

WebGL  = A 2011 minivan, okay for hauling some goods, but forget about racing
WebGPU = A 2023 electric sports car, takes off the instant you hit the accelerator

WebGPU gives the browser genuine GPU computing power, and AI model inference happens to be a GPU specialty.

How to Check if a User's Browser Supports It?

One line of code:

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

Let's break it down and chew it over:

Step 1: What is navigator.gpu?

navigator is a built-in global object in the browser, containing all sorts of information about what the browser can do. For example, navigator.geolocation is the geolocation capability, and navigator.gpu is the WebGPU capability.

If the browser supports WebGPU, navigator.gpu is a GPU object; if not, it's undefined.

Step 2: What does the !! double negation do?

!!undefined  // → false   (not supported)
!!{}         // → true    (supported, because gpu is an object)

The first ! converts the value to a boolean and negates it, the second ! negates it back. The effect is to cleanly convert any value into true or false.

Step 3: What is as any?

TypeScript's type definitions sometimes lag behind new browser features. navigator.gpu might not be in TypeScript's default types, and the compiler will throw an error saying "gpu does not exist."

as any tells TypeScript: "Stop checking, I'm sure this thing will exist at runtime, let me through."

📦 Knowledge Pack: !! is the most concise boolean conversion trick, and as any is an escape hatch to bypass TypeScript's type checking. Both are very practical and common interview questions.


Chapter 2: React 19 + Vite 8, Let's Set Up the Scaffold First

2.1 Why Choose React?

Vue is easy to pick up, but the number of projects and component libraries in the AI ecosystem for React is absurdly large. If you want to reuse someone else's AI chat components or model loading components, they are most likely written in React.

Moreover, React 19's Hooks design is particularly handy for handling the multi-state flow of "loading a model"—loading, success, failure, inferring, complete... each state gets a useState, clear and straightforward.

2.2 Seeing How React Starts from the Entry File

main.tsx is only 10 lines, but every line has a story:

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

Line-by-line translation:

Code Human Translation
createRoot(document.getElementById('root')!) Find that <div id="root"></div> in the HTML and make it React's "base camp"
! TypeScript non-null assertion: "This element 100% exists, don't report a type error"
.render(<App />) Render the <App /> component into the base camp
<StrictMode> React's development mode "inspectorate," prints extra warnings in the console to help you find potential problems

The HTML side has just one sentence:

<script type="module" src="/src/main.tsx"></script>

📦 Knowledge Point: type="module" tells the browser to load the JS as an ES Module, which is how you can use import and export in your code. Vite natively supports ES Modules during development, so it runs without bundling.


Chapter 3: React Componentization—Functions Are Components

3.1 A Vue Developer's Confusion: Where Are React's Components?

A Vue component is a .vue file with three clearly separated tags: <template>, <script>, <style>:

<!-- Vue Component: Sandwich Structure -->
<template>
  <button @click="handleClick">{{ text }}</button>
</template>

<script setup>
const handleClick = () => { console.log('clicked') }
</script>

<style scoped>
button { background: blue; }
</style>

A React component is just a JavaScript function that returns JSX (JavaScript syntactic sugar that looks like HTML):

// React Component: One function does it all
function MyButton({ text }) {
  const handleClick = () => { console.log('clicked') };

  return (
    <button onClick={handleClick} className="bg-blue-500">
      {text}
    </button>
  );
}

The Rule:

📦 Knowledge Point: React component names must start with a capital letter. In JSX, lowercase tags are treated as native HTML (<div>, <span>), while capitalized tags are treated as custom components.

3.2 Child Component: The Progress Bar

Let's look at Progress.tsx—the smallest component in the entire project, 13 lines total:

const Progress = ({text, percentage, total}) => {
    return (
        <div>
            <p>{text}</p>
            <p>{percentage}%</p>
            <p>{total}</p>
        </div>
    )
}
export default Progress

Syntactic Sugar Breakdown: {text, percentage, total} is ES6 destructuring assignment.

// Without destructuring (verbose version)
const Progress = (props) => {
    return (
        <div>
            <p>{props.text}</p>
            <p>{props.percentage}%</p>
            <p>{props.total}</p>
        </div>
    )
}

// With destructuring (elegant version) — one step to get them all
const Progress = ({text, percentage, total}) => {
    // ...
}

The parent component calls it like this:

<Progress text="model.onnx" percentage={50} total={34353543453} />

The syntax is exactly like HTML tag attributes, with zero learning curve.

📦 Knowledge Point: {curly braces} are "JavaScript expression slots" in JSX. You can write any JS expression inside the braces—variables, ternary operations, function calls... as long as it returns a value, it works.


Chapter 4: Hooks—Giving Components a "Brain"

If a component just returns a chunk of HTML, it's no different from a static image. What truly makes a component "come alive" are Hooks.

4.1 useState: The Component Gains "Memory"

const [status, setStatus] = useState(null);

Many people are baffled the first time they see this syntax. Let's break it down:

// useState returns an array with two elements
const result = useState(null);
const status = result[0];     // First element: the current value
const setStatus = result[1];  // Second element: the function to modify the value

// Use array destructuring to grab both elements in one step ← this is the shorthand for the above
const [status, setStatus] = useState(null);

Real-life Analogy:

useState = You bought a "water bottle with a display screen"

status       → The water level shown on the display (current value)
setStatus    → The faucet switch (the only way to change the water level)
useState(null) → The initial water level is empty (null)

Key Rule: You cannot directly change the value like status = "loading"—you must use setStatus("loading") to change it, so React knows "oh, the state changed, time to refresh the UI."

In the project, four state variables manage the entire application's UI changes:

const [status, setStatus]           = useState(null);
// null → initial state → show "Load Model" button
// "loading" → loading → show progress bar
// "ready" → ready → show chat interface (future implementation)

const [error, setError]             = useState<string | null>(null);
// Has a value → show red error message

const [loadingMessage, setLoadingMessage] = useState("Start Loading");
// Text displayed during loading

const [progressItems, setProgressItems] = useState([
  { text: 'model.onnx', percentage: 0, total: 34353543453 },
  { text: 'model2.onnx', percentage: 10, total: 416416416 }
]);
// List of model files, each with a name, download progress, and total size

Data-driven views work like this:

Data state changes → React detects the change → Automatically updates the UI

You don't write a single line of document.getElementById().innerHTML = xxx

4.2 Type Annotation for useState

Note this line:

const [error, setError] = useState<string | null>(null);
//                            ↑ This is TypeScript's generic annotation

<string | null> tells TypeScript: the value of this state is either a string (error message) or null (no error). If you accidentally stuff a number in there, the editor immediately warns you with a red line—catching the bug while you're writing code, not waiting until runtime.

4.3 useEffect: Automatically Do Something After the Component Is "Born"

useEffect(() => {
  console.log('The component has mounted');
}, []);

Translation: After the component renders onto the page, execute this function.

Real-life Analogy: You buy a new phone (component mounts), and it automatically runs through the initial setup after powering on (useEffect). After that, you use the phone normally, and the initial setup never runs again.

The second parameter [] (empty array) is key:

Second Parameter Meaning When It Executes
[] Depends on no variables Executes only once when the component is born
[status] Depends on status On birth + every time status changes
Not passed No limit Executes on every render (use with caution, easy to create infinite loops)

📦 Knowledge Point: The name useEffect comes from "side effect." A React component's main job is "rendering UI based on data," while extra things like "making network requests" or "operating on local storage" are "side effects," handled by useEffect.


Chapter 5: Event Interaction—From "Dashboard" to "Application"

As mentioned before, data drives the view. So what drives the data? User actions.

5.1 React's onClick

The Load Model button on App.tsx lines 123-129:

<button
  className="border px-4 py-2 rounded-lg bg-blue-400 text-white
             hover:bg-blue-500 disabled:cursor-not-allowed select-none"
  disabled={status !== null || error !== null}
  onClick={() => {
    setStatus('loading');
  }}
>
  Load Model
</button>

Point-by-point breakdown:

onClick not onclick

React uses camelCase synthetic events, not native DOM events. React wraps an event system that handles browser compatibility issues.

Remember this rule: All React events are on + camelCase event name:

Native: click  → React: onClick
Native: change → React: onChange
Native: submit → React: onSubmit

② Why wrap it in an arrow function?

This is the most common pitfall for beginners. Compare:

// ❌ Wrong: executes on render, clicking does nothing
onClick={setStatus('loading')}

// ✅ Correct: executes only on click
onClick={() => setStatus('loading')}

setStatus('loading') is a function call, it executes immediately and returns a result. Whereas () => setStatus('loading') is a function definition, it just defines "something to do later," and only executes when the user clicks.

disabled={status !== null}

The button isn't hardcoded as disabled={false}, but dynamically determined based on state:

④ TailwindCSS State Variants

hover:bg-blue-500, disabled:cursor-not-allowed—this syntax is called state variants. The part before : is the state, and the part after is the style that takes effect in that state.

5.2 Conditional Rendering: Different UIs for Different States

The error message on App.tsx line 111:

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

What is the && operation?

This is JavaScript's short-circuit evaluation:

false && <div>This will never appear</div>   // → false (doesn't render)
true  && <div>This will be displayed</div>          // → <div>This will be displayed</div>

Equivalent to:

if (error) {
  return <div>Show error message</div>
}

It's just that && is more concise, suitable for "show if exists, don't show if not" scenarios.

Similarly, the progress bar area on line 134:

{status === "loading" && (
  <div>
    {/* Render progress bar */}
  </div>
)}

📦 Core Logic Connected: User clicks button → setStatus('loading') → status changes → React re-renders → button grays out + progress bar appears. This is the complete closed loop of event → state → UI.


Chapter 6: TailwindCSS—You Don't Even Need to Write a CSS File

6.1 How Painful Is Traditional CSS?

/* Step 1: Give the element a meaningful name (think for 5 minutes) */
.model-loading-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: 400px;
  text-align: center;
  margin-bottom: 0.25rem;
}

/* Step 2: Go to the HTML and add the class */
/* Step 3: Switch back and forth between two files */
/* Step 4: Class name overwritten by a colleague, add !important */
/* Step 5: !important war */

6.2 How Does TailwindCSS Do It?

Starting from App.tsx line 67:

<div className="flex flex-col items-center mb-1 max-w-[400px] text-center">

Class names are the styles, no CSS file needed. Translation table:

Class Name Translation Corresponding CSS
flex Flex it display: flex
flex-col Arrange vertically flex-direction: column
items-center Center align align-items: center
mb-1 Bottom margin 1 unit margin-bottom: 0.25rem
max-w-[400px] Max width 400 max-width: 400px
text-center Center text text-align: center

Why is TailwindCSS so popular in the AI era?

Large models are incredibly fluent at writing TailwindCSS—they don't need to "think of a meaningful name," they just need to "translate the effect into class names."

And the entire project's CSS is just one line:

@import "tailwindcss";

TailwindCSS v4 has streamlined configuration to the extreme, no tailwind.config.js or PostCSS config needed. The Vite plugin is introduced in one line:

plugins: [react(), tailwindcss()],

Done.

6.3 className, not class

When writing JSX in a React component, use className for styles, not class:

<div className="bg-white">  ← not class="bg-white"

Because JSX is essentially JavaScript syntactic sugar, and class is a keyword for defining classes in JavaScript. To avoid conflict, React uses className instead.

Think of it as: className = "the class name for this thing is xxx."


Chapter 7: Stringing the Code Together—The Complete Data Flow

The current core logic of the entire application is just this one chain, which you can walk through in 10 seconds:

User opens the page
    ↓
navigator.gpu check → supported / not supported (if not, show a message directly)
    ↓ (supported)
Show "Load Model" button + model introduction
    ↓
User clicks button → onClick triggers → setStatus('loading')
    ↓
React detects status change → re-renders
    ↓
Button grays out (disabled) + progress bar area appears
    ↓
(Future: model download complete → setStatus('ready') → chat interface appears)

What each line of code does:

File Lines Role
index.html 11 lines Provides an empty shell <div id="root">
main.tsx 10 lines "Mounts" the React application onto that empty shell
App.tsx 160 lines The brain of the entire application—state, logic, UI all here
Progress.tsx 13 lines A pure display component, only responsible for showing progress info
index.css 1 line Imports TailwindCSS
vite.config.ts 8 lines Tells Vite: use React plugin + TailwindCSS plugin

Chapter 8: Small Details in the Project You Might Overlook

8.1 Two Uses of the ! Non-Null Assertion

// Use 1: A DOM element definitely exists
createRoot(document.getElementById('root')!);

// Use 2: A value is definitely not null/undefined
const value = possiblyNull!;

⚠️ Think carefully when using it—if the assertion is wrong, it will explode at runtime. This is a "gentleman's agreement" between you and TypeScript.

8.2 map to Iterate and Render a List

{progressItems.map(({text, percentage, total}, index) => {
  return (
    <Progress
      key={index}
      text={text}
      percentage={percentage}
      total={total}
    />
  )
})}

8.3 disabled={status !== null || error !== null}

This isn't a hardcoded true or false, but a dynamically calculated boolean expression. The UI automatically follows the data—this is the charm of declarative programming.


Conclusion: The New Identity of a Frontend Engineer

Looking back at this project, you might think it's still "incomplete"—the logic for model downloading and inference hasn't been written in yet, just the UI framework.

But the value of a framework lies in setting up the infrastructure well.

When @huggingface/transformers (HuggingFace's JavaScript SDK, which allows the browser to load and infer AI models) is integrated, the moment status changes from "loading" to "ready", it becomes an application that can truly run DeepSeek-R1 in the browser.

You don't need to be an AI researcher to build AI products. The role of a frontend engineer in the AI era is not to be replaced, but to package AI capabilities into products that users can actually use.

Using the React + TypeScript + TailwindCSS you're familiar with, plus a little WebGPU knowledge, you can build an AI application that needs no backend, no server, no API key, and runs even when offline.

🍺 The boundaries of frontend are far wider than you think.


Appendix: If an Interviewer Asks These Questions After Seeing Your Resume

Possible Question You Can Now Answer
"What's the principle behind useState?" React uses closures and linked lists to manage each component's state queue. Calling the setter triggers a re-render.
"Why use !! instead of Boolean()?" The effect is the same, !! is more concise; however, some team coding standards require explicit use of Boolean() for better readability.
"What are the downsides of TailwindCSS?" HTML class names get very long (but can be solved by splitting components or using a cn() utility function); requires familiarity with the class name mappings.
"What's the difference between WebGPU and WebGL?" WebGL was designed for graphics rendering, WebGPU is designed for general-purpose GPU computing, making the latter more suitable for AI inference.
"What are React synthetic events?" React's own wrapped event system, providing browser compatibility and performance optimization (event delegation to the root), with an API consistent with native events.

Project code: ai_doubao_dcx: Towards AGI, Towards Doubao - Gitee.com
Start: pnpm install && pnpm dev
Browser: Chrome/Edge 113+