React + WebGPU 在浏览器运行 DeepSeek:从 Worker 通信到流式生成
React + WebGPU 在浏览器运行 DeepSeek:从 Worker 通信到流式生成
本文基于
webgpu-deepseek项目源码整理,重点解释模型如何在浏览器中加载、推理和返回结果。源码静态阅读,运行未验证;实际 WebGPU 兼容性、模型下载情况和生成速度需要在目标环境单独确认。
你会得到什么
这个项目不是简单地在页面里调用一个模型,而是拆成了三层:
- React 主线程:负责输入框、聊天列表和加载进度。
- Web Worker:负责下载模型、初始化 WebGPU 和执行推理。
- Transformers.js:负责 tokenizer、模型加载和文本生成。
核心判断是:模型生命周期和页面交互要分开管理,缓存和流式消息是浏览器端运行大模型的关键。
1. 先看完整调用链
main.tsx ↓ 挂载 App App.tsx ↓ 创建 Worker,发送 check/load/generate worker.js ↓ 检测 WebGPU ↓ 加载 tokenizer 和 model ↓ TextStreamer 流式生成 ↓ postMessage 返回状态和文本 App.tsx ↓ 更新 React state Chat.jsx ↓ Markdown、HTML 安全清理、数学公式渲染主线程和 Worker 之间不是直接调用函数,而是约定消息格式:
| 消息类型 | Worker 行为 | 页面用途 |
|---|---|---|
check | 检查 WebGPU 适配器 | 判断能力 |
load | 下载并初始化模型 | 显示加载进度 |
generate | 生成回答 | 显示流式文本 |
interrupt | 中断生成 | 响应停止按钮 |
reset | 清理缓存和中断状态 | 开始新的状态 |
2. 为什么模型放进 Web Worker
App.tsx创建了一个 module Worker:
worker.current=newWorker(newURL("./worker.js",import.meta.url),{type:"module",});worker.current.postMessage({type:"check"});页面主线程擅长处理 DOM 和用户交互,但模型下载、WebGPU 初始化和推理都可能是耗时任务。Worker 可以把这些工作放到后台线程,主线程只接收结果并更新 UI。
Worker 中不能直接使用window、document操作页面,因此它通过:
self.postMessage({status:"update",output,});把结果发送给 React。
这里有一个需要重点记住的地方:postMessage不是普通函数调用。主线程发送的是一份消息数据,Worker 再根据type判断要做什么。
3. WebGPU 检查分两步
页面中有快速判断:
constIS_WEBGPU_AVAILABLE=!!navigator.gpu;它只说明浏览器是否提供了navigator.gpu属性。
Worker 中还会继续请求适配器:
constadapter=awaitnavigator.gpu.requestAdapter();if(!adapter){thrownewError("WebGPU is not supported (no adapter found)");}可以把两者理解为:
!!navigator.gpu:有没有 WebGPU 入口。requestAdapter():能不能找到实际可用的 GPU 适配器。
所以第一个判断为true,并不代表后续模型推理一定成功。浏览器版本、显卡驱动、模型格式和显存都可能影响结果。
4.TextGenerationPipeline如何避免重复加载
项目用一个类统一管理 tokenizer 和模型:
classTextGenerationPipeline{staticmodel_id="onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX";staticasyncgetInstance(progress_callback=null){this.tokenizer??=AutoTokenizer.from_pretrained(this.model_id,{progress_callback,});this.model??=AutoModelForCausalLM.from_pretrained(this.model_id,{dtype:"q4f16",device:"webgpu",progress_callback,});returnPromise.all([this.tokenizer,this.model]);}}static做了什么
static让getInstance属于类本身,因此可以直接调用:
TextGenerationPipeline.getInstance();??=做了什么
this.model??=loadModel();只有this.model为null或undefined时才加载。第一次调用会下载和初始化,后续调用复用原来的 Promise 或模型对象。
这体现了“单例式缓存”思想:模型初始化成本高,生成多次回答时不应该反复加载。
两个参数的含义
dtype:"q4f16",device:"webgpu",源码意图是使用量化数据类型降低资源压力,并让模型运行在 WebGPU 设备上。具体兼容性和性能不能只靠静态代码判断,本文不把它们描述成已验证结果。
5. 从聊天消息到模型输入
用户消息最终通过:
constinputs=tokenizer.apply_chat_template(messages,{add_generation_prompt:true,return_dict:true,});转换为模型需要的输入。
messages是聊天结构,例如:
[{role:"user",content:"请解释 Web Worker"},]模型真正处理的不是这段普通字符串,而是 tokenizer 转换后的 token 数据。
add_generation_prompt: true的作用是补充生成提示,让模型知道接下来应该由 assistant 回答。
6.TextStreamer为什么能实现流式输出
模型生成不是一次性返回全部文本,而是不断生成 token。项目配置了:
conststreamer=newTextStreamer(tokenizer,{skip_prompt:true,skip_special_tokens:true,callback_function,token_callback_function,});其中:
callback_function:获得已经转换好的文本片段,并发送给主线程。token_callback_function:每生成 token 时统计数量和速度。skip_prompt:不重复显示输入提示词。skip_special_tokens:隐藏特殊 token。
发送给页面的消息大致是:
self.postMessage({status:"update",output,tps,numTokens,state,});React 收到update后,把output追加到最后一条 assistant 消息,因此用户能看到逐步生成的回答。
7. 思考过程和答案如何区分
代码通过编码<think></think>,拿到开始和结束 token:
const[START_THINKING_TOKEN_ID,END_THINKING_TOKEN_ID]=tokenizer.encode("<think></think>",{add_special_tokens:false,});当生成到结束思考 token 时:
if(tokens[0]==END_THINKING_TOKEN_ID){state="answering";}前端根据answerIndex把内容拆成 thinking 和 answer,并允许用户展开或收起思考过程。
8. 页面渲染为什么需要 DOMPurify
Chat.jsx的渲染链是:
模型 Markdown 文本 ↓ marked.parse HTML 字符串 ↓ DOMPurify.sanitize 安全一些的 HTML ↓ dangerouslySetInnerHTML 插入 React 页面关键代码:
constresult=DOMPurify.sanitize(marked.parse(text,{async:false,breaks:true,}),);Markdown 转 HTML 后,如果直接使用dangerouslySetInnerHTML,就需要考虑危险 HTML 内容。项目先使用 DOMPurify 清理,这是一个重要的安全边界。
另外,MathJax负责数学公式显示,适合模型回答方程、代码解释等内容。
9. 模型加载与生成的两个阶段
加载阶段
发送 load ↓ 发送 loading ↓ getInstance 下载 tokenizer 和 model ↓ 发送下载进度 ↓ 用简单输入生成 1 个 token 进行预热 ↓ 发送 ready预热的目的,是提前触发模型和 WebGPU 的初始化工作,让正式提问时少承担一部分首次初始化成本。实际耗时和效果需要运行验证。
生成阶段
发送 generate ↓ reset stopping_criteria ↓ 准备 chat template ↓ model.generate ↓ TextStreamer 持续发送 update ↓ 发送 complete用户点击停止时,发送interrupt,Worker 调用:
stopping_criteria.interrupt();这是一种由生成过程主动检查停止条件的中断设计。
10. 排错清单
| 现象 | 优先检查 |
|---|---|
navigator.gpu类型警告 | 是否安装并配置@webgpu/types;不要长期依赖as any |
| Worker 无法加载 | new URL引用的文件名是否和src中实际文件一致 |
Failed to resolve import | package.json是否声明对应依赖,包管理器是否混用 |
| 页面一直不能输入 | Worker 是否发送ready,主线程是否正确设置status |
| 只有完整结果没有实时输出 | TextStreamer是否传入streamer,是否处理update |
| Markdown 渲染异常 | marked输入、反斜杠处理和 MathJax 配置 |
| HTML 安全风险 | 是否先调用DOMPurify.sanitize |
| 停止按钮无效 | stopping_criteria是否传给model.generate |
结语
这个项目最值得迁移的设计不是某一个 API,而是职责划分:React 处理交互,Worker 管理重任务,模型类负责资源生命周期,消息状态负责跨线程反馈。理解这条调用链后,再学习 WebGPU、tokenizer 或流式生成,都会更容易定位问题。
建议下一步按以下顺序实践:先单独完成 Worker 的消息往返,再接入 tokenizer,最后接入模型和流式 UI。本文代码和项目运行结果均未验证,部署前应补做依赖安装、构建、浏览器 WebGPU 能力和模型加载检查。
标签: React, WebGPU, Transformers.js, Web Worker