从零到一:在浏览器中运行 DeepSeek-R1,深度解析端侧大模型与 React + WebGPU 实战
前言
你是否曾经遇到过这样的困扰:调用大模型 API 价格高昂,企业数据通过网络传输存在安全隐患,或者想在离线环境下使用 AI 能力却无从下手?其实有一种更优雅的方案------端侧模型 ,也就是把 LLM 直接部署在用户的设备上。今天,我将带你一步步搭建一个运行在浏览器里的 DeepSeek-R1 蒸馏版推理 Demo,只依靠 React + TailwindCSS + Transformers.js,全程无需服务器,数据绝对安全。
在这次实战中,代码中的每一行注释都藏着重要的知识点。我们不仅会实现功能,还会逐条解读这些注释背后的技术原理:从函数式组件的"数据驱动"思想 ,到 Hooks 的状态与副作用 ,再到 TailwindCSS 的原子类哲学,以及 WebGPU 的硬件加速检测。完整代码和踩坑经验全部奉上,让我们一起探索"AI + 前端"的无限可能。

🚨 剧透预警 :这只是整个项目的第一部分!目前我们完成了界面骨架和状态体系,下一篇文章将带你加上底部的模型下载进度条,让加载过程一目了然。记得关注,别错过续集哦~
一、为什么需要端侧大模型?
在传统的大模型应用架构中,我们通常会通过 API 调用远程的 LLM 服务,比如 OpenAI 或 DeepSeek 的云端接口。这种方式存在两个致命问题:
- 成本高:按 Token 计费,长期使用对企业或个人都是一笔不小的开销。
- 不安全:所有上下文(context)数据都会随请求发送到服务器,敏感信息有泄露风险。
而端侧模型(On-device Model)将 LLM 直接部署在用户的设备上,比如手机、车载系统,甚至是浏览器里。它的核心优势:
- 数据隐私有保障:所有推理都在本地完成,数据不出设备。
- 离线可用:一次下载模型后,即使断网也能正常使用。
- 低成本:无需按量付费,尤其适合高频简单任务。
- 低延迟:跳过了网络传输,响应速度更快。
当然,端侧模型对硬件有一定要求,通常我们使用开源小参数模型 (例如 1.5B 量级),它们经过蒸馏和优化后,足以完成翻译、摘要、简单对话等任务。而在浏览器端运行,则可以借助 WebGPU 硬件加速,实现随时随地下载、随时使用的极致轻量化体验。
📌 小贴士:想要在本地玩转端侧模型,可以先用 Ollama 体验一键部署开源模型的快乐。它支持 Windows/Linux/macOS,一行命令就能把 Llama、Qwen 等模型跑在本地。但今天我们更进一步,让模型直接跑在浏览器标签页里!
二、技术选型:React + TypeScript + TailwindCSS + WebGPU
现代大型 AI 项目的前端,几乎绕不开 React。和 Vue 相比,React 的上手门槛略高,但凭借其灵活的函数式编程范式和庞大的生态,成为了大模型应用的首选。特别是函数式组件 + Hooks的组合,让数据和 UI 的关系变得异常清晰------你只需要定义状态,React 就会自动帮你更新界面。
对比 Vue 的 SFC(template + script + style 三明治结构),React 组件本质是一个返回 JSX 的函数,JS 逻辑和 HTML 结构天然融合在一起,CSS 则通过独立的样式文件或 TailwindCSS 这种原子化工具注入。
技术栈一览
- React 18 + TypeScript :强类型约束 + 函数式组件 + Hooks(
useState,useEffect等) - Vite:极速构建工具,原生支持 JSX/TSX
- TailwindCSS:原子化 CSS 框架,几乎不再需要手写 CSS
- ESLint:代码风格统一,大公司协作必备
- Transformers.js + ONNX Runtime Web:在浏览器中加载和运行蒸馏版大模型
- WebGPU:调用本地 GPU 硬件加速推理
三、项目初始化:从一行命令开始
打开终端,用 Vite 模板一把梭创建 React + TypeScript 项目:
bash
npm init vite@latest deepseek-webgpu -- --template react-ts
cd deepseek-webgpu
npm install
生成的项目已经配置好了 ESLint,保证代码风格一致------这对于团队项目或开源项目尤为重要。比如它会强制你使用单引号还是双引号,缩进是 2 空格还是 4 空格,告别混乱的代码格式。
四、引入 TailwindCSS:告别传统 CSS 的低效
传统 CSS 需要定义选择器、编写属性值,然后还要小心处理优先级和样式冲突。这就像是用二进制直接编程,底效且容易出错。 TailwindCSS 带来了一种"原子化"的思维:它提供了成千上万个语义化的类名(比如 flex, text-center, bg-white),你只需在 HTML 标签上组合这些类名,就能快速构建出复杂的界面。
安装并配置 TailwindCSS(基于 Vite 插件):
bash
npm install -D tailwindcss @tailwindcss/vite
修改 vite.config.ts:
typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
})
在入口 CSS 文件(src/index.css)顶部添加:
css
@import 'tailwindcss';
现在你可以在组件里直接使用类似 className="flex flex-col items-center" 的原子类了。注意,JSX 中我们用 className 而非 class,因为 class 是 JavaScript 的关键字(用于声明类),所以 React 选择了 className 替代,这算是 JSX 语法的一个小细节。
五、React 函数组件:搭积木式的 UI 构建
React 将界面拆分成一个个独立的"积木"------组件。每个组件其实就是一个返回 JSX 的函数,函数内部可以写任何 JavaScript 逻辑,最后返回一段看起来像 HTML 的 JSX 代码。
理解 JSX:JSX 是 JavaScript 与 XML 的结合,它让我们可以在 JS 文件中直接书写 HTML 标签,编译后会被转成原生的 DOM 操作。这是 React 最骄傲的特性之一,让 UI 表达变得直观而高效。
看看入口文件 src/main.tsx 是如何挂载组件的:
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>,
)
<App /> 就是我们自己的顶层组件,它会被渲染到页面 ID 为 root 的 div 中。整个应用就像一棵由组件构成的树,层层嵌套。
六、核心组件 App.tsx 逐步拆解(注释深度解析版)
下面就是我们的主角------App.tsx。我将带你结合上课笔记,逐行剖析其中的关键知识点。特别注意代码中的注释,它们比代码本身更重要------每一条注释都是对设计思想、API 原理和工程实践的提炼。
1. 理解现代前端框架的本质
现代前端框架的三大核心概念:组件化 、响应式 和 数据绑定。
组件化 :无论是 React 还是 Vue,都将界面拆分成独立、可复用的组件。每个组件都像一个黑盒子,内部封装了自身的结构(HTML)、样式(CSS)和交互逻辑(JS)。在 React 中,这个"黑盒子"就是一个函数;在 Vue 中,它是一个 .vue 单文件。这种封装性让我们可以像搭积木一样组合出复杂页面,极大地提升了代码的可维护性和复用性。
响应式 :这是现代框架与 jQuery 时代最本质的区别。我们不再需要手动查找 DOM 节点并修改其属性,而是声明式地描述"当数据为 X 时,UI 应该长什么样"。框架会自动追踪数据的变化,并高效地更新对应的 DOM 节点。React 通过 useState 触发重新渲染,Vue 则通过 ref 和 reactive 的 Proxy 拦截实现。响应式机制让开发者可以专注于数据和业务逻辑,而不必陷入繁琐的 DOM 操作。
数据绑定 :数据绑定是连接界面与数据的桥梁。在 JSX 中,我们使用 {} 进行单向数据绑定:数据流向视图。例如 <p>{count}</p>,当 count 变化时,<p> 的内容会自动更新。Vue 则支持双向绑定(v-model),但 React 推崇单向数据流,这样做更易于追踪状态变化,避免数据混乱。数据驱动视图的理念,不需要去做 DOM 编程 -> 数据状态(响应式,修改状态,界面就会跟着变)。

"函数封装特性"则强调了 React 的组件就是一个函数,它具备了函数的天然优势:接收参数、返回结果、可组合、可复用。这种以函数为最小单元的抽象方式,让 UI 的构建过程变得像编写普通的 JavaScript 逻辑一样自然。
2. 引入 Hooks:函数式思想的精髓
tsx
import {
useState , // react 函数式思想 hooks 每个函数都有一个hooks 钩子状态,以use开头
useEffect , // 生命周期钩子函数 组件挂载时执行
} from 'react'
useState注释解读 :React 的函数组件本身是无状态的,但通过useState这个 Hook,我们可以为组件"挂载"响应式状态。就像给函数装了一个钩子,让它能够记住数据。凡是名字以use开头的函数,都是 React 的 Hook,它们只能在组件函数的最顶层调用。useEffect注释解读 :useEffect被称为副作用 Hook,它可以模拟类组件中的生命周期方法。当组件挂载(首次插入 DOM)时、状态更新时、甚至销毁时,都可以触发里面的逻辑。第二行的注释"组件挂载时执行"只是一个常见用法,实际上它的触发时机由依赖数组决定。
3. 函数组件的结构注释:职责分明的代码骨架
一个函数组件的标准写法:
- 头上挂 Hooks :组件顶部调用各种
useState、useEffect等,声明状态和副作用。 - 中间写逻辑:处理数据、事件、条件判断等。
- 最后吐 JSX:返回一段 JSX 模板,描述 UI 的结构。
React 组件就是这样一个纯函数,输入数据,输出界面,干净且可预测。
4. 状态设计的哲学:UI = f(state)
tsx
const [status, setStatus] = useState<string | null>(null); // 响应式数据状态
这段注释极其重要,它揭示了 React 的核心理念------数据驱动视图。
- 传统 DOM 编程 vs 响应式 :以前我们需要手动选择 DOM 元素,修改其
innerText或className。而现在,我们只需要定义好状态(如status),并描述每种状态下界面应该长什么样,React 会自动完成 DOM 更新。 - 状态枚举 :
null(尚未开始)、loading(加载中)、ready(就绪)、error(失败)......这些状态就像川剧变脸的面具,数据一变,界面瞬间切换,用户看到的就是当前时刻的"快照"。 - 类型约束 :
useState<string | null>(null)通过泛型指定了状态只能是字符串或null,TypeScript 会帮我们严格把关。

5. 错误状态与加载进度:让反馈更细腻
tsx
const [ error, setError] = useState<Error | null>(null); // 错误对象数据状态
const [ loadingMessage, setLoadingMessage ] = useState(""); // 加载中状态
const [ progressItem, setProgressItem ] = useState([{
file:'model.onnx',
progress:0,
total:66666666,
}]); // 进度状态
error存储一个 JavaScriptError对象,可以从中提取message和stack,实现精确的错误提示。loadingMessage提供动态加载文字,比如"正在下载模型文件...",缓解等待焦虑。progressItem跟踪模型文件(如 ONNX 格式的model.onnx)的下载进度,progress和total分别为已下载和总字节数。这里敲黑板:progressItem就是为下篇文章的下载进度条预留的! 目前界面中还没有渲染它,但状态已经就位,下一篇我们会在页面底部加一条酷炫的缓存进度条,让加载过程一目了然。
6. WebGPU 能力检测:双重否定的技巧
tsx
// 浏览器 导航栏 是否支持 WebGPU
// 现代浏览器的重要特性
// ! 取反 navigator.gpu 不支持的时候 undefined
// !! 再取反,一定苦于变成 true | false
// 双重否定等于肯定
const IS_WEBGPU_AVAILABLE = !!navigator.gpu;
这段注释生动地解释了双重否定操作:
navigator.gpu是 WebGPU 的入口对象,如果浏览器不支持,它为undefined。- 一个
!将其转换为布尔值并取反:undefined→true,对象→false。 - 两个
!再取反一次,就得到了原始的布尔值:undefined→false,对象→true。 - 所以
!!等价于Boolean()强制类型转换,常用于将"可能为假值"的变量转为确切的true/false。这里得到true表示浏览器支持 WebGPU,可以继续后续的硬件加速推理。
7. useEffect 的挂载与副作用
tsx
// useEffect 用,status 状态变化时执行
// 组件的生命周期 副作用
// 副作用 组件挂载后,附带做什么
useEffect(() => {
console.log('组件已经挂载完成')
// setTimeout(() => {
// setStatus('ready')
// }, 2000)
}, [status])
- 副作用(Side Effect) 指的是那些与组件渲染无关但又必须执行的操作,比如数据请求、订阅、手动修改 DOM 等。这里的
console.log就是一个简单的副作用。 - 依赖数组
[status]:表示只有当status的值发生变化时,useEffect内的函数才会重新执行。如果依赖数组为空[],则只在组件首次挂载时执行一次,类似componentDidMount。 - 注释中提到用
setTimeout模拟状态变化,从null变成ready,这正是我们后续加载模型时需要实现的核心流程。
8. 组件函数的执行与状态变更
tsx
console.log('组件函数执行')
// js 脚本 数据逻辑交互
// count 数据状态
// 修改count setCount
const [count, setCount] = useState(0) // 响应式状态 ref
这里特意引入了一个未在 UI 中使用的 count 状态,目的是演示 React 的重渲染机制。每当通过 setCount 改变状态时,整个 App 函数会被重新调用,控制台会再次打印"组件函数执行"。但注意,count 的值会被 React 保留并递增,因为 useState 底层通过链表维护了组件的所有状态。这个实验证明:函数组件虽然是普通的 JavaScript 函数,但在 React 的运行环境中拥有了"记忆"能力。
9. JSX 中的条件渲染与布局注释
tsx
return (
// 原子类,组合一下
IS_WEBGPU_AVAILABLE ? (
<div className="flex flex-col h-screen mx-auto items-center justify-center text-gray-800 bg-white">
-
条件渲染 :
IS_WEBGPU_AVAILABLE ? (...) : (...)三元运算符是最直接的条件渲染方式,根据 WebGPU 支持情况展示不同的完整界面。这种写法保证了每个分支的 UI 描述都是完整的,易于阅读。 -
原子类组合 :
flex flex-col h-screen mx-auto items-center justify-center这一串类名实现了一个灵活的垂直居中布局:flex开启 Flex 布局flex-col主轴纵向h-screen高度占满整个视口mx-auto水平方向自动外边距(居中)items-center交叉轴居中(横向居中)justify-center主轴居中(纵向居中)
-
注意 JSX 的注释格式 :在 JSX 中,注释需要用
{/* 注释内容 */}包裹,而不能使用普通的//或/* */,因为 JSX 最终会被编译为 JavaScript 调用,普通注释会被当作文本节点处理。
10. 标题区域注释:模型信息与社区链接
tsx
{/* 标题区域 */}
<div className="flex flex-col items-center mb-4 max-w-[400px] text-center">
{/* 蒸馏的是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>
- 蒸馏(Distillation):注释指出模型是蒸馏自 Qwen 架构。知识蒸馏是一种模型压缩技术,它用一个大的"教师模型"教导小的"学生模型",使得小模型在保持接近大模型精度的同时,参数量大幅减少,非常适合端侧部署。

11. 模型介绍区域:开源生态的黄金三角
tsx
<a
href="https://huggingface.co/onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX"
target="_blank"
rel="noreferrer"
className="font-medium underline"
>
{/* DeepSeek-R1 的 15 亿参数量蒸馏版,用 Qwen 架构,适合本地轻量推理。
蒸馏Qwen Reasoning 推理
HuggingFace 抱抱脸 全球最大开源模型社区
*/}
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 是huggingface 提供的js 库,用于加载和推理模型
}
<a
href="https://huggingface.co/docs/transformers.js"
target="_blank"
rel="noreferrer"
className="underline"
>
🤗 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{" "}
这一段注释极富信息量,堪称整个 Demo 的"生态说明书":
- DeepSeek-R1-Distill-Qwen-1.5B:15 亿参数的推理模型,专为本地轻量推理优化,特别擅长复杂的推理任务(Reasoning)。
- HuggingFace 🤗 :全球最大的开源模型社区,提供了模型托管、版本管理、推理库等一系列工具,被开发者亲切地称为"抱抱脸"。我们使用的模型就托管在它的
onnx-community组织下。 - Transformers.js:HuggingFace 官方出品的 JavaScript 推理库,让 Transformer 模型能在浏览器中运行,无需后端。它内部会调用 ONNX Runtime Web 或者直接使用 WebGPU。
- ONNX Runtime Web:开放神经网络交换格式的运行时,支持 WebGPU 后端,能进一步加速模型推理。ONNX 是一种通用的模型存储格式,可以让训练好的模型在不同框架和硬件上运行。
- 隐私承诺:所有计算都在浏览器本地完成,数据不发送到任何服务器,甚至可以离线使用。这是端侧模型最大的魅力所在。
12. 错误界面:响应式错误提示
tsx
{/* 报错界面状态,响应式 */}
{
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>
)}
- 利用
error && (...)实现条件渲染:只有当error不为null时,错误提示卡片才会显示。因为error是一个响应式状态,一旦它被设置,组件会立刻重新渲染,用户会即时看到错误信息。这种"即变即显"的体验,是传统 DOM 操作难以优雅实现的。
七、模型加载流程:Transformers.js 与 ONNX Runtime 联动
在 useEffect 或其他生命周期中,我们实际会调用 Transformers.js 的管道 API 来下载模型和分词器等资源。同时利用 setProgressItem 更新下载进度,setStatus 切换状态。一旦模型就绪,status 变为 ready,用户即可开始对话。
在当前的 UI 阶段,我们还没有实现真正的推理逻辑,但所有状态和界面骨架已经完整搭建。后续只需要在点击按钮或输入文本时,调用模型管道并更新 loadingMessage 或对话历史即可。

八、总结与展望
通过这个项目,我们串联起了端侧大模型的核心概念、React 函数式组件与 Hooks、TailwindCSS 原子化样式,以及 WebGPU 硬件加速的实战运用。但更重要的是,我们逐条品味了代码中的注释------它们是思想的凝结,是知识的注解。
你学到了什么?
- 端侧模型的价值与适用场景,数据安全永远第一。
- React 函数组件 = Hooks(状态+副作用)+ 逻辑 + JSX,简单而强大。
- 注释是一种工程素养:好的注释能解释"为什么这么做",而不只是"做了什么"。
- 原子类与响应式思维,让 UI 开发变得像写自然语言一样流畅。
- WebGPU 为浏览器端的高性能 AI 推理打开了新大门。
未来,端侧模型将在物联网、自动驾驶、工业边缘计算等领域大放异彩。而浏览器作为最普适的终端,必将成为 AI 能力分发的重要阵地。希望这篇从零开始的实战文章,能帮你推开这扇门,用代码和注释一起构建更智能的应用。

九、下集预告:进度条来了!🚀
到此为止,我们的 DeepSeek-R1 WebGPU 项目已经搭建好了完整的 UI 骨架和状态体系。界面上能看到标题、模型介绍链接,也能根据 WebGPU 支持情况和错误状态做出响应------唯独还差底部的模型下载进度条。
别急,这恰好是我们下篇文章的重头戏!到时候我们会:
- 在页面底部加入一条优雅的进度条组件
- 利用
progressItem状态实时更新下载百分比 - 配合
loadingMessage显示动态提示文字 - 让整个加载体验从"干等"变成"可视化"的享受
嘿,感谢你读到这里! 🎉 如果这篇文章让你对端侧 AI 和 React 有了新的火花,别吝啬你的点赞和收藏,顺手转发给同样在折腾大模型前端的小伙伴吧~ 有什么脑洞大开的端侧 AI 玩法,或者踩过的坑,评论区就是你的舞台 ,咱们一起交流、一起进步!别忘了点个关注,下篇进度条实战已经在路上了,不见不散!👋