云计算百科
云计算领域专业知识百科平台

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 = new Worker(new URL("./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 检查分两步

页面中有快速判断:

const IS_WEBGPU_AVAILABLE = !!navigator.gpu;

它只说明浏览器是否提供了 navigator.gpu 属性。

Worker 中还会继续请求适配器:

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error("WebGPU is not supported (no adapter found)");
}

可以把两者理解为:

  • !!navigator.gpu:有没有 WebGPU 入口。
  • requestAdapter():能不能找到实际可用的 GPU 适配器。

所以第一个判断为 true,并不代表后续模型推理一定成功。浏览器版本、显卡驱动、模型格式和显存都可能影响结果。

4. TextGenerationPipeline 如何避免重复加载

项目用一个类统一管理 tokenizer 和模型:

class TextGenerationPipeline {
static model_id =
"onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX";

static async getInstance(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,
});

return Promise.all([this.tokenizer, this.model]);
}
}

static 做了什么

static 让 getInstance 属于类本身,因此可以直接调用:

TextGenerationPipeline.getInstance();

??= 做了什么

this.model ??= loadModel();

只有 this.model 为 null 或 undefined 时才加载。第一次调用会下载和初始化,后续调用复用原来的 Promise 或模型对象。

这体现了“单例式缓存”思想:模型初始化成本高,生成多次回答时不应该反复加载。

两个参数的含义

dtype: "q4f16",
device: "webgpu",

源码意图是使用量化数据类型降低资源压力,并让模型运行在 WebGPU 设备上。具体兼容性和性能不能只靠静态代码判断,本文不把它们描述成已验证结果。

5. 从聊天消息到模型输入

用户消息最终通过:

const inputs = 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。项目配置了:

const streamer = new TextStreamer(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 页面

关键代码:

const result = 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

赞(0)
未经允许不得转载:网硕互联帮助中心 » React + WebGPU 在浏览器运行 DeepSeek:从 Worker 通信到流式生成
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!