你有没有想过,有一天打开一个网页就能直接跑通一个 1.5B 参数的大语言模型?不用买显卡、不用配环境、不用注册账号,甚至断网了都能用。

最近我在做一个叫 DeepSeek-R1 WebGPU 的项目,把 DeepSeek-R1 的 1.5B 蒸馏版模型搬到了浏览器里,全程用 WebGPU 加速,前端用 React + TypeScript + Tailwind CSS。这篇文章我会把整个项目的技术细节掰开揉碎讲清楚。
你可能已经用过 ChatGPT 或者 DeepSeek 的网页版。输入问题,等待几秒,答案就出来了。体验不错对吧?但这里面有几个不太容易被注意到的问题。
第一个问题是成本。 大模型 API 是按 token 计费的——token 可以理解成模型处理文字时的最小单位,一个汉字大概对应 1 到 2 个 token。你问得越多、聊得越长,费用就越高。
第二个问题是隐私。 每一次你向 API 发送请求,你的问题和上下文都会被传送到远端的服务器。如果你问的是公司内部的代码、客户的敏感信息、或者任何不方便被第三方看到的内容,这个模式就有点令人担忧了。
第三个问题是网络依赖。 没网就彻底歇菜。
那有没有办法让模型跑在本地、不联网、还不用花钱?有的。这就是端侧模型的思路——把模型直接部署在用户的设备上(手机、电脑、汽车的中控系统),推理计算全部在本地完成。
浏览器是端侧模型最轻量的分发平台。用户点开一个网页,模型自动下载到本地,推理全程在浏览器里跑,数据不出设备,用完即走。
但问题来了:浏览器的性能够跑大模型吗?
大模型推理的本质就是大量的矩阵乘法运算——你可以把它想象成无数个数字表格在不停地做乘法和加法。这种运算恰恰是 GPU(图形处理器)最擅长的事情。
过去浏览器想用 GPU 算力,主要靠 WebGL。但 WebGL 本来是为画 3D 图形设计的,用来做通用计算属于“跨界打工”,效率并不高。
WebGPU是新一代的 Web 标准,专门为图形渲染和通用计算设计。它让网页开发者能直接调用系统 GPU 进行高性能计算。简单说,WebGPU 就是浏览器里的 GPU 计算接口,类似 CUDA 的 Web 版。
有了 WebGPU,浏览器里也能做矩阵乘法 → 也就能跑 Transformer 模型 → 也就能跑大语言模型。
截至 2024 年 10 月,WebGPU 的全球浏览器支持率大约在 70% 左右。Chrome 113+、Edge 113+ 都已经支持,Firefox 和 Safari 需要通过实验性开关开启。
在代码里检测浏览器是否支持 WebGPU,只需要一行:
typescript
const IS_WEBGPU_AVAILABLE = !!navigator.gpu;
navigator.gpu是浏览器提供的 API 入口点。如果浏览器支持 WebGPU,这个属性会返回一个GPU对象;如果不支持,返回undefined。!!的作用是把任何值都转成布尔值——undefined变成false,有值变成true。
项目搭好架子之后,下一步就是选技术栈。我选了三个核心工具:React、TypeScript、Tailwind CSS。
传统网页开发里,HTML 管结构、CSS 管样式、JavaScript 管逻辑——三者是分离的。这种模式在简单页面里很好用,但页面一复杂,改一个按钮可能要同时修改三个文件,维护起来很头疼。
React 换了一种思路:把 HTML、CSS、JavaScript 打包成一个独立的功能单元,叫做“组件” 。一个按钮是一个组件,一个输入框是一个组件,整个聊天界面也是一个组件。组件可以嵌套、可以复用,像搭积木一样拼出完整的页面。
在 React 里,组件就是一个 JavaScript 函数。这个函数返回一段叫做 JSX 的代码——JSX 看起来像 HTML,但实际上是 JavaScript 的语法扩展。
下面是一个最简单的 React 组件:
tsx
function Welcome() {return <h1>你好,世界</h1>;}
这个函数返回了一个<h1>标签,当你在页面上使用<Welcome />时,浏览器就会渲染出“你好,世界”这个标题。
普通的 JavaScript 变量改了之后,页面不会自动更新。你需要手动操作 DOM(文档对象模型)去修改页面内容——比如document.getElementById('xxx').innerText = '新内容'。
React 的useState解决了这个问题。它创建了一个“响应式”的数据状态:状态变了,界面会自动跟着变。
tsx
import { useState } from 'react';function Counter() {// 声明一个叫 count 的状态,初始值是 0// setCount 是修改这个状态的函数const [count, setCount] = useState(0);return (<div><p>你点击了 {count} 次</p><button onClick={() => setCount(count + 1)}>点我</button></div>);}
这段代码做了什么?useState(0)创建了一个初始值为 0 的状态。点击按钮时调用setCount(count + 1),count变成 1,页面上的{count}自动更新为 1。你不需要写任何 DOM 操作代码。
在 DeepSeek-R1 项目里,我用了多个状态来管理模型加载的不同阶段:
tsx
const [status, setStatus] = useState(null);// 模型状态:null | 'loading' | 'ready'const [error, setError] = useState(null);// 错误信息const [loadingMessage, setLoadingMessage] = useState("");// 加载进度提示
status一开始是null,表示模型还没开始加载。开始加载后变成'loading',加载完成变成'ready'。界面根据status的不同值显示不同的内容——这就是“数据驱动界面”的核心思想。
useEffect是 React 的另一个核心 Hook(钩子函数),用来处理“副作用”。什么是副作用?任何不属于“渲染界面”本身的事情——比如发请求、操作浏览器 API、读写本地存储等。
useEffect接收两个参数:一个函数(你要做的事情),和一个依赖数组(控制什么时候做)。
tsx
import { useEffect } from 'react';function App() {useEffect(() => {// 这个函数会在组件第一次渲染到屏幕上之后执行console.log('组件已经挂载完成');// 这里可以做一些初始化的事情,比如加载模型}, []);// 空数组表示只在组件挂载时执行一次}
在 DeepSeek-R1 项目里,useEffect用来在页面加载后自动开始下载和初始化模型。[]这个空数组保证了初始化逻辑只在组件第一次出现时执行一次,不会重复运行。
JavaScript 是动态类型语言——变量可以是数字,也可以是字符串,还可以随时改变类型。灵活是灵活了,但大型项目里这种灵活性往往会变成隐患。
TypeScript给 JavaScript 加上了类型系统。你可以明确告诉编译器:这个变量是数字、那个参数是字符串、这个函数返回布尔值。
typescript
// 变量名后面跟 : 类型 就是类型注解let name: string = "张三";// name 只能是字符串let age: number = 25; // age 只能是数字let isReady: boolean = true;// isReady 只能是 true 或 false// 函数参数和返回值也可以加类型function greet(person: string): string {return "你好," + person;}
如果你试图把数字赋值给name,TypeScript 编译器会报错。这听起来有点啰嗦,但在大型项目里,类型系统能帮你提前发现很多低级错误,也让代码的意图更清晰。
传统 CSS 开发里,你要给每个元素起一个类名,然后在单独的 CSS 文件里写样式。类名一多,维护就变得困难。
Tailwind CSS换了一种方式:它提供了大量细粒度的工具类(也叫原子类),每个类只做一件事。你想让元素有内边距,就用p-4;想让它居中,就用text-center;想让它水平排列,就用flex。
tsx
// 不用单独写 CSS 文件,直接在 className 里组合原子类<div className="flex flex-col items-center max-w-[400px] text-center"><h1 className="text-4xl font-bold mb-1">DeepSeek-R1 WebGPU</h1></div>
flex flex-col让子元素纵向排列,items-center让它们在交叉轴方向居中,text-4xl设置大号字体。你不需要离开 JSX 文件就能完成所有样式工作。
团队协作时,每个人写代码的习惯不一样——有人用单引号有人用双引号,有人加分号有人不加。这些小差异累积起来会让代码库变得杂乱。
ESLint是一个代码检查工具,它可以根据你配置的规则自动检查代码风格,甚至自动修复。大公司的前端项目几乎都会配置 ESLint,保证代码风格统一。
聊完前端技术栈,终于到了最核心的问题:模型到底怎么在浏览器里跑?
Transformers.js是 Hugging Face 推出的 JavaScript 库,让你能在浏览器或 Node.js 环境里直接运行预训练的 AI 模型。
它在设计上尽量对标 Python 版的 Transformers 库,但底层实现不同——Transformers.js 使用ONNX Runtime来执行模型推理。
不同 AI 框架(PyTorch、TensorFlow 等)训练出来的模型格式各不相同。ONNX(Open Neural Network Exchange,开放神经网络交换格式)是一个开放标准,让模型可以在不同框架和平台之间迁移。
ONNX Runtime 是专门用来执行 ONNX 模型的高性能推理引擎。ONNX Runtime Web 则是它的浏览器版本。
Transformers.js 只支持 ONNX 格式的模型。好消息是,Hugging Face 的模型库里已经有大量转换好的 ONNX 模型可以直接使用。如果你想用自己的模型,也可以用 Optimum 工具把 PyTorch 或 TensorFlow 模型转成 ONNX 格式。
这个项目加载的是DeepSeek-R1-Distill-Qwen-1.5B-ONNX。
来拆解一下这个名字:
15 亿参数听起来很多,但在大模型世界里其实算“轻量级”。经过蒸馏和量化之后,这个模型已经能在浏览器里跑起来了。
在代码里加载模型大致是这样的(简化版):
typescript
import { pipeline } from '@huggingface/transformers';// 创建文本生成管道// pipeline 函数会下载模型、加载到内存、准备推理const generator = await pipeline('text-generation',// 任务类型:文本生成'onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX', // 模型名称{ device: 'webgpu' }// 使用 WebGPU 加速);// 执行推理const result = await generator('你好,请介绍一下自己');console.log(result);
pipeline是 Transformers.js 提供的最简单的使用方式——它把预处理、模型推理、后处理全部打包在一起。你只需要告诉它“我要做什么任务”和“用哪个模型”,剩下的它帮你搞定。
{ device: 'webgpu' }这个配置告诉 Transformers.js 用 WebGPU 来加速计算。如果没有这个配置,模型会默认用 CPU 运行,速度会慢很多。
现在我们把所有知识点串起来,看看 DeepSeek-R1 项目的App.tsx组件完整长什么样,逐行拆解。
tsx
import { useState, useEffect } from 'react';function App() {// 1. 声明三个响应式状态const [status, setStatus] = useState(null);const [error, setError] = useState(null);const [loadingMessage, setLoadingMessage] = useState("");// 2. 检测浏览器是否支持 WebGPUconst IS_WEBGPU_AVAILABLE = !!navigator.gpu;// 3. 组件挂载后执行初始化useEffect(() => {console.log('组件已经挂载完成');// 这里会触发模型的下载和加载}, []);// 4. 根据 WebGPU 支持情况渲染不同内容return IS_WEBGPU_AVAILABLE ? (<div className="flex flex-col h-screen mx-auto items-center justify-end"><div className="flex flex-col items-center"><h1 className="text-4xl font-bold">DeepSeek-R1 WebGPU</h1><h2>一个在浏览器本地运行的推理模型</h2></div>{/* 状态和错误信息的展示 */}{error && (<div className="text-red-500"><p>加载模型失败:</p><p>{error}</p></div>)}</div>) : (<div>您的浏览器还不支持 WebGPU</div>);}export default App;
逐行来看:
第 1 行:从 React 库中导入useState和useEffect两个 Hook。
第 4-9 行:App组件是一个函数。函数内部用useState声明了三个状态——status追踪模型加载进度,error记录错误信息,loadingMessage显示加载提示。
第 12 行:IS_WEBGPU_AVAILABLE是一个常量,通过!!navigator.gpu检测浏览器是否支持 WebGPU。
第 15-18 行:useEffect在组件挂载后执行。这里会发起模型加载的请求(实际代码中会更复杂,涉及异步加载和进度更新)。
第 21-39 行:return语句返回 JSX。三元运算符? :根据IS_WEBGPU_AVAILABLE的值决定渲染哪个界面——支持 WebGPU 就显示主界面,不支持就显示提示信息。
className里的flex、flex-col、items-center、text-4xl、font-bold都是 Tailwind CSS 的原子类,分别控制布局方向、对齐方式、字体大小和粗细。
这个项目把几个关键技术串在了一起:
打开网页就能跑大模型——这已经不是什么未来概念了。随着 WebGPU 浏览器支持率的提升和端侧模型性能的持续优化,浏览器正在变成一个越来越强大的 AI 计算平台