DeepSeek-R1 WebGPU (1):在浏览器里跑大模型
文章目录
一份保姆级技术复盘,覆盖端侧模型、React + TypeScript、TailwindCSS、JSX 等核心技能点,适合学习复盘和技术分享。
一、端侧模型:AI 不再只活在云端
1.1 什么是端侧模型?
平时我们使用 ChatGPT、DeepSeek、Kimi 等 AI 助手,流程是这样的:
用户输入 → 网络请求 → 远程服务器(GPU集群) → 推理计算 → 返回结果
这种方式叫云端推理,模型跑在厂商的服务器上。它有两个绕不开的问题:
- 贵:厂商需要采购大量 GPU,成本最终转嫁给你(API 按 token 计费)。
- 不安全:你的输入内容(context)会随着请求发送到远端服务器,数据隐私无法完全掌控。
而端侧模型(On-Device Model)指的是模型直接运行在你的设备上——手机、电脑、汽车、甚至浏览器。数据不出设备,推理在本地完成。
1.2 为什么端侧模型突然火了?
关键推动力来自两点:
| 推动因素 | 说明 |
|---|---|
| 开源小参数模型成熟 | Llama、Qwen、Gemma 等 1B~7B 参数模型,在特定任务上表现已经不输大模型 |
| WebGPU 的到来 | 浏览器可以直接调用 GPU 做并行计算,不再依赖 WebGL 的"曲线救国" |
Ollama 就是典型的端侧方案——你下载模型到本地,通过命令行或 API 调用。而本项目的更进一步:模型直接在浏览器里下载、加载、推理,用户打开网页就能用,用完即走,不占用磁盘。
1.3 本项目的模型选择
项目使用的是 DeepSeek-R1-Distill-Qwen-1.5B:
- 这是 DeepSeek-R1(推理模型)的蒸馏版,参数量压缩到 15 亿。
- 基于 Qwen 架构,专为本地轻量推理优化。
- 模型格式为 ONNX(Open Neural Network Exchange,开放神经网络交换格式),跨平台跨框架。
- 托管在 HuggingFace(全球最大开源模型社区),通过
Transformers.js加载。
关键理解:蒸馏 = 用大模型"教"小模型。大模型生成高质量答案 → 小模型模仿学习 → 保留大部分推理能力但体积小很多。
二、React + TypeScript:为什么是 AI 时代的首选?
2.1 React vs Vue:选型的底层逻辑
你可能会问:Vue 上手更简单,为什么 AI 项目偏爱 React?
| 维度 | React | Vue |
|---|---|---|
| 学习曲线 | 较陡(需要理解 JSX、Hooks、函数式编程) | 平缓(模板语法接近 HTML) |
| 大型项目 | 函数式编程天然适合抽象和复用,生态更成熟 | 中小项目效率极高 |
| AI/ML 生态 | Transformers.js、LangChain.js、Vercel AI SDK 都优先支持 React | 社区也在跟进,但目前示例偏少 |
| 招聘市场 | 大厂、AI Startup 的首选 | 国内中小企业用得更多 |
一句话总结:React 的上限更高,Vue 的下限更低。做 AI 相关的复杂交互,React 的函数式思想更适合。
2.2 新建项目:React + TS + ESLint 一步到位
# 使用 Vite 创建项目(最快的构建工具)
npm create vite@latest webgpu-demo -- --template react-ts
cd webgpu-demo
npm install
创建完成后,你会得到以下关键文件:
webgpu-demo/
├── src/
│ ├── App.tsx # 主组件(你写代码的地方)
│ ├── App.css # 组件样式
│ ├── main.tsx # 入口文件(挂载 React 到页面)
│ └── index.css # 全局样式 + Tailwind 导入
├── eslint.config.js # ESLint 代码约束配置
├── vite.config.ts # Vite 构建配置
├── tsconfig.json # TypeScript 配置
└── package.json # 依赖管理
package.json 的核心依赖解读:
{
"dependencies": {
"@tailwindcss/vite": "^4.3.3", // TailwindCSS Vite 插件
"react": "^19.2.6", // React 核心库
"react-dom": "^19.2.6", // React DOM 渲染(浏览器端)
"tailwindcss": "^4.3.3" // TailwindCSS 框架本体
},
"devDependencies": {
"typescript": "~6.0.2", // TypeScript 编译器
"eslint": "^10.3.0", // 代码规范检查
"vite": "^8.0.12" // 构建工具
}
}
ESLint 的作用是什么?
ESLint 是代码"纪律委员"——约束团队写出一致风格的代码。比如用单引号还是双引号?结尾要不要分号?这些规则在 eslint.config.js 中统一配置。大公司必备,否则代码合并时就是灾难。
// eslint.config.js 关键配置
export default defineConfig([
globalIgnores(['dist']), // 忽略构建产物
{
files: ['**/*.{ts,tsx}'], // 对 TS 和 TSX 文件生效
extends: [
js.configs.recommended, // JS 基础规则
tseslint.configs.recommended, // TypeScript 规则
reactHooks.configs.flat.recommended, // React Hooks 规则
],
},
])
2.3 Vite 配置:让 Tailwind 跑起来
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
react(), // 让 Vite 支持 React JSX
tailwindcss(), // 让 Vite 处理 Tailwind 原子类
],
})
Vite 插件机制很简单:每个插件负责一块能力,像搭积木一样拼起来。react() 负责编译 JSX,tailwindcss() 负责扫描和注入 CSS。
三、TailwindCSS:告别手写 CSS 的原子化方案
3.1 传统 CSS 的痛点
回想一下你写 CSS 的流程:
- 想一个 class 名(
.my-cool-button) - 找到对应文件(或
<style>块) - 写选择器 + 规则(
color: red; font-size: 16px;) - 反复调试样式冲突和优先级
这个过程太低效了——你在两个文件之间来回切换,还要想命名、管优先级。
3.2 Tailwind 的思路:原子类
Tailwind 的做法是:不写 CSS 规则,直接写类名。
<!-- 传统方式 -->
<button class="my-button">点击</button>
<style>
.my-button {
background: blue;
color: white;
padding: 8px 16px;
border-radius: 4px;
}
</style>
<!-- Tailwind 方式 -->
<button className="bg-blue-500 text-white px-4 py-2 rounded">
点击
</button>
每一个 class 名 = 一条 CSS 规则。bg-blue-500 就是 background-color: blue,px-4 就是 padding-left: 1rem; padding-right: 1rem;。
为什么这更好?
- 不用命名:不用再纠结 class 叫
btn-primary还是btn-main - 所见即所得:看到类名就知道样式,不用跳转到 CSS 文件
- 自然语言友好:类名是用英文单词组合的,和 AI 编程(Vibe Coding)天然契合
- 按需生成:Vite 插件只提取你用到的类名,打包体积很小
3.3 Tailwind 运行原理
Tailwind 不是原生 CSS——浏览器不认识 bg-blue-500。它的工作流程是:
1. 你写 className="bg-blue-500 text-white"
↓
2. Tailwind Vite 插件扫描所有 .tsx/.jsx 文件
↓
3. 识别到 bg-blue-500 → 找到对应 CSS: background-color: #3b82f6;
↓
4. 把这条 CSS 注入到最终构建的样式文件中
↓
5. 浏览器正确渲染蓝色背景
核心原理一句话:Tailwind 是一个"类名到 CSS 规则"的映射字典。插件在构建时扫描代码 → 查字典 → 生成最小化的 CSS 文件。你没有用到的类名不会出现在最终产物中。
在项目中的体现:
/* src/index.css — 只需要一行! */
@import "tailwindcss";
/* 下面是项目自定义的 CSS 变量和全局样式 */
:root {
--text: #6b6375;
--bg: #fff;
/* ... */
}
@import "tailwindcss" 这一行就是 Tailwind 的"入口",插件会从这里开始注入扫描到的所有原子类。
3.4 为什么是 className 而不是 class?
这是一个非常经典的困惑。答案很简单:
JSX 中写 <div class="xxx"> 会出问题,因为 class 是 JavaScript 的关键字(用于定义类/面向对象编程)。
React 团队为了避免语法冲突,用 className 替代了 class:
// ❌ 错误:class 是 JS 关键字
<div class="container">
// ✅ 正确:使用 className
<div className="container">
编译后 <div className="container"> → 原生 DOM 的 <div class="container">,效果一模一样。
四、React 组件:函数就是积木
4.1 Vue 组件 vs React 组件
Vue 组件是"三件套"——HTML、CSS、JS 分块写在一个 .vue 文件里:
<template>
<div>{{ message }}</div>
</template>
<script setup>
const message = 'Hello'
</script>
<style scoped>
div { color: red; }
</style>
React 组件就是一个函数,返回 HTML(JSX):
function MyComponent() {
const message = 'Hello' // JS 逻辑
// CSS 通过 import 或 Tailwind 引入
return <div>{message}</div> // 返回 HTML
}
两者的本质区别:
| Vue | React | |
|---|---|---|
| 组件形态 | .vue 单文件(模板+逻辑+样式) |
函数(JS + JSX) |
| 入门难度 | 低(模板接近原生 HTML) | 中(需要理解 JSX 和函数式编程) |
| 抽象能力 | 指令体系(v-if, v-for) | JavaScript 原生能力(&&, map) |
React 的理念:组件就是函数,函数就是组件。所有 JavaScript 的能力(条件判断、循环、解构)都能直接在"模板"里用。
4.2 入口文件:React 是怎么启动的?
// src/main.tsx — React 应用的"点火开关"
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css' // 全局样式(含 Tailwind)
import App from './App.tsx' // 导入根组件
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
执行流程:
createRoot(...)— 找到index.html中的<div id="root">,把它变成 React 的"根容器".render(...)— 把<App />组件渲染到这个容器里<StrictMode>— 开发模式下的"严格检查",会帮你发现潜在问题(比如不安全的生命周期),生产环境自动失效
五、代码详解:App.tsx 逐段解析
下面逐一解析 App.tsx 的每一部分代码,确保你完全理解。
5.1 导入 Hooks
import { useState, useEffect } from 'react'
useState:React 的"状态钩子"。让你在函数组件中创建响应式数据——数据变了,界面自动更新。useEffect:React 的"副作用钩子"。组件渲染完成后自动执行指定代码(比如发请求、设置定时器)。- 这两个函数都以
use开头,这是 React 的约定——所有 Hooks 都遵循useXxx命名模式。
5.2 数据状态:响应式的核心
function App() {
// status: 当前加载状态
// null = 初始 / 'loading' = 加载中 / 'ready' = 模型就绪
const [status, setStatus] = useState(null)
// error: 错误信息(演示用 "出错了" 作为初始值)
const [error, setError] = useState("出错了")
// loadingMessage: 加载提示文本
const [loadingMessage, setLoadingMessage] = useState("")
// progressItems: 模型文件下载进度
const [progressItems, setProgressItems] = useState([{
file: 'model.onnx', // 模型文件名
progress: 0, // 当前已下载字节数
total: 5465458632 // 模型总大小(约 5.5GB)
}])
useState 语法详解:
const [值, 修改值的函数] = useState(初始值)
这是数组解构语法——useState 返回一个长度为 2 的数组:
- 第一个元素是当前状态值(只读,不要直接修改)
- 第二个元素是更新函数(想改状态?调它!)
// ❌ 错误:直接修改不会触发界面更新
status = 'ready'
// ✅ 正确:调用更新函数
setStatus('ready') // 状态变了 → React 自动重新渲染组件
为什么叫"响应式"? 数据(状态)和界面是绑定的。就像川剧变脸——你切换一张脸谱(改状态),观众看到的脸就变了(界面更新)。你不需要手动操作 DOM,React 帮你做好了。
5.3 WebGPU 检测:一行代码判断浏览器能力
const IS_WEBGPU_AVAILABLE = !!navigator.gpu
这行代码值得拆开理解:
| 表达式 | 含义 |
|---|---|
navigator.gpu |
浏览器是否暴露 GPU 接口。支持 WebGPU → 返回对象;不支持 → undefined |
!navigator.gpu |
取反。支持 → false;不支持 → true |
!!navigator.gpu |
再取反(双重否定等于肯定)。支持 → true;不支持 → false |
!! 是一种将任意值强转为布尔值的 JS 技巧:
!!{} // true
!!undefined // false
!!null // false
!!0 // false
!!'hello' // true
5.4 组件生命周期:useEffect 的执行时机
useEffect(() => {
console.log('组件已经挂载完成')
setTimeout(() => {
// setStatus('ready') // 1 秒后将状态改为 ready
}, 1000)
}, []) // ← 空数组,只执行一次
useEffect 的第二个参数是关键:
| 第二个参数 | 执行时机 |
|---|---|
[](空数组) |
组件首次渲染后执行一次 |
[status] |
首次渲染后 + status 变化后执行 |
| 不传 | 每次渲染后都执行 |
这里的 [] 意味着"组件挂载完成时执行,只此一次"——非常适合做初始化操作(加载模型、请求数据等)。
5.5 JSX:在 JavaScript 里写 HTML
return (
IS_WEBGPU_AVAILABLE ? (
<div className="flex flex-col h-screen ...">
<h1 className="text-4xl font-bold mb-1">DeepSeek-R1 WebGPU</h1>
{/* ... */}
</div>
) : (
<div>您的浏览器还不支持WebGPU</div>
)
)
JSX(JavaScript XML) 是 React 最骄傲的特性之一——在 JS 代码中直接写 HTML 标签。
几个 JSX 核心规则:
① 条件渲染:三目运算符
{condition ? <ComponentA /> : <ComponentB />}
② 列表渲染:.map()
{items.map(item => <li key={item.id}>{item.name}</li>)}
③ 嵌入 JS 表达式:{} 大括号
<p>计算结果:{1 + 1}</p> // → 计算结果:2
<p>用户名:{user.name}</p> // → 用户名:张三
④ 注释:大括号包裹
{/* 这是 JSX 注释,和 JS 多行注释一样的写法 */}
⑤ 条件显示:&& 短路
{error && (
<div className="text-red-500">
<p>Unable to load model due to the following error:</p>
<p className="text-sm">{error}</p>
</div>
)}
当 error 为空字符串或 null 时,&& 右边不执行,整个 <div> 不渲染。这是 React 中极常用的条件渲染模式。
5.6 Tailwind 原子类实战解读
来看看项目中用到的关键原子类:
<div className="flex flex-col h-screen mx-auto items-center justify-end text-gray-800 bg-white">
| 类名 | 对应 CSS | 含义 |
|---|---|---|
flex |
display: flex |
开启弹性布局 |
flex-col |
flex-direction: column |
主轴方向为垂直(从上到下) |
h-screen |
height: 100vh |
高度 = 整个屏幕高度 |
mx-auto |
margin-left: auto; margin-right: auto |
水平居中 |
items-center |
align-items: center |
子元素垂直居中 |
justify-end |
justify-content: flex-end |
子元素靠底部对齐 |
text-gray-800 |
color: #1f2937 |
文字颜色 |
bg-white |
background-color: white |
背景色 |
自定义值的语法:
<div className="max-w-[400px]"> {/* 方括号内是自定义值 */}
[] 允许你使用 Tailwind 预设之外的任意值。这里 max-w-[400px] 等价于 max-width: 400px。
1rem = 4 是 Tailwind 的默认尺寸单位映射:p-1 = 4px,p-4 = 16px,以此类推。
5.7 模型信息展示区解析
<p className="mx-w-[510px] mb-4">
You are about to load
<a
href="https://huggingface.co/onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX"
target="_blank"
rel="noreferrer"
className="font-medium underline"
>
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
<a
href="https://huggingface.co/docs/transformers.js"
target="_blank"
rel="noreferrer"
className="underline"
>
🤗 Transformers.js
</a>
and ONNX Runtime Web, meaning no data is sent to a server.
</p>
两个关键链接指向的技术:
- DeepSeek-R1-Distill-Qwen-1.5B-ONNX:模型托管在 HuggingFace。HuggingFace 是全球最大的开源模型社区,被称为 AI 界的 GitHub。
- Transformers.js:HuggingFace 推出的 JavaScript 库,让你在浏览器中加载和推理 Transformer 模型,无需后端服务。
- ONNX Runtime Web:微软的 ONNX 运行时浏览器版,负责在 WebGPU 上高效执行模型推理。
这两个库配合 WebGPU,让"浏览器跑大模型"从不可能变成了现实。
5.8 错误处理状态
{error && (
<div className="text-red-500 text-center mb-2">
<p className="mb-1">Unable to load model due to the following error:</p>
<p className="text-sm">{error}</p>
</div>
)}
当 error 有值时(非空字符串),显示红色错误提示。当错误被清除(setError(null) 或 setError('')),错误提示自动消失。这就是"响应式条件渲染"——你只需要改数据,界面自己会跟着变。
六、全文总结
本文从一个真实的浏览器端 AI 推理项目出发,系统梳理了以下技术链路:
- 端侧模型:模型从云端走向本地,从服务器走向浏览器。核心理念是"数据不出设备",WebGPU 是浏览器端 AI 的关键基础设施。
- React + TypeScript:AI 时代大型前端项目的首选技术栈。函数式组件 + Hooks 模式提供了强大的抽象能力。
- TailwindCSS:原子化 CSS 框架,用"堆类名"替代"写 CSS",开发效率翻倍。Vite 插件在构建时按需注入样式。
- React 组件化:函数 = 组件,JSX = 模板。所有 JavaScript 能力直接用于 UI 表达。
- 状态驱动:
useState+useEffect实现响应式数据绑定,数据变化自动驱动界面更新。
七、核心知识点复盘
| 序号 | 知识点 | 一句话总结 |
|---|---|---|
| 1 | 端侧模型 | LLM 运行在用户设备上,数据不出设备,隐私安全 |
| 2 | ONNX | 开放神经网络交换格式,跨框架跨平台的模型标准 |
| 3 | HuggingFace | 全球最大开源模型社区,AI 界的 GitHub |
| 4 | WebGPU | 浏览器原生 GPU API,替代 WebGL 做高性能计算 |
| 5 | useState |
React 状态钩子,创建响应式数据 [值, 更新函数] |
| 6 | useEffect |
React 副作用钩子,组件渲染后执行,第二个参数控制执行时机 |
| 7 | !! |
双重否定强转布尔值,!!undefined = false,!!{} = true |
| 8 | JSX | JavaScript XML,在 JS 中写 HTML,React 的核心语法 |
| 9 | className |
JSX 中替代 class(因为 class 是 JS 关键字) |
| 10 | Tailwind | 原子化 CSS 框架,类名即样式,按需生成,不写 CSS 文件 |
| 11 | Vite 插件 | 扩展 Vite 能力(处理 JSX、Tailwind 等),像搭积木 |
| 12 | ESLint | 代码约束工具,确保团队代码风格一致 |
| 13 | 条件渲染 | {condition && <Component />} 或三目运算符 |
| 14 | 响应式 | 数据变化 → 界面自动更新,无需手动操作 DOM |
八、常见问题 / 避坑指南
Q1:!!navigator.gpu 和 Boolean(navigator.gpu) 有区别吗?
没有本质区别,效果一样。!! 更简洁,是 JS 社区的惯用写法。不推荐 new Boolean()。
Q2:useEffect 第二个参数传空数组 [] 时,函数什么时候执行?
组件首次挂载到 DOM 后执行一次。类比 Vue 的 mounted() 生命周期钩子。
Q3:为什么不直接在 useState 里写 useState(null) => useState("出错了") 会怎样?
不会怎样,初始值只是"第一次渲染时"的状态。后续通过 setError 更新。这里给 "出错了" 是为了演示错误状态 UI。
Q4:Tailwind @import "tailwindcss" 报错怎么办?
检查 vite.config.ts 中是否添加了 tailwindcss() 插件。Tailwind v4 通过 Vite 插件工作,不需要手动安装 PostCSS。
Q5:为什么组件函数里 console.log 会执行多次?
React 在开发模式(StrictMode)下会故意渲染两次来帮你发现副作用问题。生产环境不会。这是正常的,不用担心。
Q6:模型文件 5.5GB,浏览器怎么存得下?
模型通过 Transformers.js 分片下载后会缓存在浏览器的 Cache Storage 中。第二次访问时直接从缓存加载,不需要重新下载。离线也能用。
项目地址:github.com/onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX
技术栈:React 19 + TypeScript 6 + Vite 8 + TailwindCSS 4 + WebGPU + Transformers.js + ONNX Runtime Web
更多推荐



所有评论(0)