DeepSeek-R1 WebGPU (1):在浏览器里跑大模型
文章目录
- 一、端侧模型:AI 不再只活在云端
- 1.1 什么是端侧模型?
- 1.2 为什么端侧模型突然火了?
- 1.3 本项目的模型选择
- 二、React + TypeScript:为什么是 AI 时代的首选?
- 2.1 React vs Vue:选型的底层逻辑
- 2.2 新建项目:React + TS + ESLint 一步到位
- 2.3 Vite 配置:让 Tailwind 跑起来
- 三、TailwindCSS:告别手写 CSS 的原子化方案
- 3.1 传统 CSS 的痛点
- 3.2 Tailwind 的思路:原子类
- 3.3 Tailwind 运行原理
- 3.4 为什么是 `className` 而不是 `class`?
- 四、React 组件:函数就是积木
- 4.1 Vue 组件 vs React 组件
- 4.2 入口文件:React 是怎么启动的?
- 五、代码详解:App.tsx 逐段解析
- 5.1 导入 Hooks
- 5.2 数据状态:响应式的核心
- 5.3 WebGPU 检测:一行代码判断浏览器能力
- 5.4 组件生命周期:useEffect 的执行时机
- 5.5 JSX:在 JavaScript 里写 HTML
- 5.6 Tailwind 原子类实战解读
- 5.7 模型信息展示区解析
- 5.8 错误处理状态
- 六、全文总结
- 七、核心知识点复盘
- 八、常见问题 / 避坑指南
一份保姆级技术复盘,覆盖端侧模型、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 创建项目(最快的构建工具)npmcreate vite@latest webgpu-demo ----templatereact-tscdwebgpu-demonpminstall创建完成后,你会得到以下关键文件:
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 关键配置exportdefaultdefineConfig([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.tsimport{defineConfig}from'vite'importreactfrom'@vitejs/plugin-react'importtailwindcssfrom'@tailwindcss/vite'exportdefaultdefineConfig({plugins:[react(),// 让 Vite 支持 React JSXtailwindcss(),// 让 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 规则,直接写类名。
<!-- 传统方式 --><buttonclass="my-button">点击</button><style>.my-button{background:blue;color:white;padding:8px 16px;border-radius:4px;}</style><!-- Tailwind 方式 --><buttonclassName="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'// true5.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