ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

React + WebGPU 在浏览器运行 DeepSeek:从 Worker 通信到流式生成

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 中不能直接使用windowdocument操作页面,因此它通过:

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做了什么

staticgetInstance属于类本身,因此可以直接调用:

TextGenerationPipeline.getInstance();

??=做了什么

this.model??=loadModel();

只有this.modelnullundefined时才加载。第一次调用会下载和初始化,后续调用复用原来的 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 importpackage.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

返回列表