WebGPU DeepSeek项目实战(一):从Hugging Face模型链路到Web Worker通信框架
WebGPU DeepSeek项目实战(一):从Hugging Face模型链路到Web Worker通信框架
前言
最近我在做一个名为 webgpu-deepseek 的项目,目标是让 DeepSeek 模型直接运行在浏览器中,也就是端侧模型。它和常见的 AI 聊天页面有一个很大的区别:传统网页通常把用户问题发送给服务器,由服务器上的 GPU 调用模型,再把答案返回给浏览器;这个项目则希望把模型下载到浏览器本地,利用 Transformers.js、Web Worker 和 WebGPU 在用户设备上完成推理。
这个方向同时连接了前端与 AI 两套知识体系。前端部分涉及 React、TypeScript、Vite、Web Worker 和浏览器 API,AI 部分则涉及 Hugging Face、模型文件、Tokenizer、推理管线与 GPU 加速。直接从模型加载代码开始抄,很容易出现“页面跑起来了,但不知道每一层为什么存在”的情况。因此,这个系列不会急着堆功能,而是按照项目实际开发顺序逐步推进。
这是系列的第一部分。我们会从模型来源和依赖安装开始,一步步写出 WebGPU 判断、Worker 创建、消息监听和模型加载入口。每讲清一个概念,项目就向前推进一步;模型生成、流式输出和中断任务会沿用这套通信结构继续扩展。
我平时会通过《你不知道的 JavaScript》补语言基础,通过掘金等技术社区了解工程实践,关注 AI 开发者的分享,再去 GitHub 阅读真实项目源码。学习过程中最重要的一步,是把理解后的内容重新输出到社区:只有能够把执行流程讲清楚,才说明自己不只是“见过这段代码”。
这一步的目标:让浏览器成为模型的运行环境,让 React 负责交互,让 Web Worker 负责耗时任务,让 WebGPU 负责并行计算。
1. 项目要解决什么问题
1.1 为什么尝试在浏览器中运行模型
常见的大模型应用采用服务端推理:浏览器把问题发送到接口,服务器加载模型、执行推理,再把结果返回。它的优点是模型能力强、硬件由平台统一管理,但也意味着应用依赖网络与服务器资源。
浏览器本地推理提供了另一条路线。模型首次下载完成后,可以缓存在用户设备中,再次打开页面时不一定需要重新下载全部文件。推理过程发生在本地,也能减少部分数据离开设备的需求。不过,这条路线同样有明确限制:模型文件较大、首次加载慢、浏览器内存有限,而且 WebGPU 兼容性仍需要认真检查。
| 对比维度 | 服务端模型推理 | 浏览器本地模型推理 |
|---|---|---|
| 模型运行位置 | 云服务器或远程 GPU | 用户浏览器和本地 GPU |
| 首次使用成本 | 通常无需下载模型 | 需要下载模型文件 |
| 网络依赖 | 每次请求通常都需要网络 | 模型缓存后可减少远程下载 |
| 数据流向 | 输入一般需要发送到服务器 | 推理可以留在用户设备 |
| 硬件控制 | 服务端统一配置 | 受用户浏览器和显卡能力影响 |
| 适合模型 | 可以承载较大的模型 | 更适合压缩、量化或蒸馏模型 |
这个项目选择 DeepSeek-R1-Distill-Qwen-1.5B,正是因为端侧环境更关注模型体积和设备承受能力。1.5B 表示模型大约具有 15 亿参数;“Distill”表示它通过蒸馏方式继承更大推理模型的部分能力。它仍然不是一个很小的网页资源,所以第一次下载和初始化会比较慢。
1.2 Hugging Face、ModelScope 与模型社区
Hugging Face 是 AI 开源生态中最有影响力的模型社区之一。模型厂商和开发者可以在 Hub 上发布模型权重、Tokenizer、配置文件、模型说明与使用示例。应用不需要把全部模型文件提交进前端代码仓库,只要知道模型 ID,就可以按需访问对应仓库。
国内开发者也经常使用 ModelScope(魔搭社区)。两者都承担模型托管、发现和协作的作用,但具体模型格式、生态工具和下载方式可能不同。这个项目选择 Hugging Face 与 Transformers.js 的组合,因此代码围绕 Hugging Face Hub 的模型组织方式展开。
模型进入浏览器的大致链路如下:
DeepSeek-R1-Distill-Qwen-1.5B
↓ 导出适合浏览器推理的 ONNX 文件
Hugging Face 模型仓库
↓ Transformers.js 根据模型 ID 请求文件
浏览器首次下载
↓ 写入浏览器缓存
Tokenizer + 模型初始化
↓ WebGPU 执行并行计算
文本生成等 NLP 任务
这里要区分“模型社区”和“模型运行库”。Hugging Face Hub 负责保存和分发模型,Transformers.js 才是浏览器中加载、执行模型的工具。可以把 Hub 理解为仓库,把 Transformers.js 理解为把仓库内容取下来并运行的引擎。
1.3 Transformers.js 在链路中的位置
@huggingface/transformers 是 Transformers.js 的 npm 包。它提供 JavaScript 版本的 Transformer 模型加载与推理能力,让前端可以通过模型 ID 远程访问 Hugging Face 模型,并执行文本生成、文本分类、特征提取、语音识别等 NLP 或多模态任务。
Transformers.js 默认可以从 Hugging Face Hub 下载适配的 ONNX 模型,并在浏览器支持时使用缓存。第一次加载需要传输模型、Tokenizer 和配置文件,所以耗时明显;再次加载可以尝试复用浏览器缓存,因此通常会更快。但浏览器缓存仍受存储额度、清理策略和站点来源影响,不能把它理解成永远不会丢失的本地文件。
WebGPU 则位于执行链路的后半段。它允许网页以更接近现代 GPU 的方式提交计算任务,适合机器学习中的矩阵运算。Transformers.js 可以借助 ONNX Runtime Web 把部分模型计算交给 WebGPU,从而避免完全依赖 CPU。
模型 ID 解决“去哪里找模型”,Transformers.js 解决“怎样加载并运行模型”,浏览器缓存解决“避免每次重新下载”,WebGPU 解决“怎样更高效地计算”。
相关概念可以结合 Transformers.js Pipeline 官方文档、Transformers.js WebGPU 指南 和 MDN WebGPU API 继续核对。
2. 项目依赖与工程工具
2.1 运行时依赖分别负责什么
package.json 中的运行时依赖如下。我们先认识每个依赖在整条链路中的职责,再在对应功能出现时使用它,避免刚开始就把所有 API 堆进代码。
"dependencies": {
"@huggingface/transformers": "3.7.1",
"@tailwindcss/vite": "^4.3.3",
"better-react-mathjax": "^2.0.3",
"dompurify": "^3.2.3",
"marked": "^15.0.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwindcss": "^4.3.3"
}
| 依赖 | 作用 | 在项目中的位置 |
|---|---|---|
@huggingface/transformers |
JavaScript 版本的 Transformers 库,负责加载 Tokenizer、模型并执行推理 | Worker 中的模型管线 |
react |
使用组件、状态和 Hooks 构建交互界面 | App.tsx |
react-dom |
把 React 组件挂载到浏览器 DOM | main.tsx |
tailwindcss |
提供原子化 CSS 类 | 页面 className |
@tailwindcss/vite |
把 Tailwind 接入 Vite 构建流程 | vite.config.ts |
marked |
把 Markdown 文本解析成 HTML | 模型回答渲染 |
dompurify |
清理 HTML 中潜在的不安全内容 | Markdown HTML 安全处理 |
better-react-mathjax |
在 React 页面中显示数学公式 | 模型公式渲染 |
marked 的作用尤其容易被误解。大模型返回内容时经常使用 Markdown,因为 Markdown 能自然表达标题、代码块、加粗、列表和引用。浏览器最终显示的是 HTML,因此需要先完成格式转换。
例如,模型返回:
# 一元二次方程
解析后对应的 HTML 结构是:
<h1>一元二次方程</h1>
Markdown 比直接生成 HTML 更简洁,也更适合流式文本。但解析得到 HTML 后不能盲目信任内容,因此项目同时安装了 dompurify。前者负责“转换”,后者负责“清理”,两者职责不同。
2.2 开发依赖与 TypeScript 工具链
开发依赖不会直接成为页面业务功能,但它们决定了项目如何开发、检查和构建。
| 依赖 | 主要作用 |
|---|---|
vite |
提供开发服务器、模块处理和生产构建 |
typescript |
在开发阶段进行静态类型检查,最终代码仍会构建为 JavaScript |
@vitejs/plugin-react |
让 Vite 正确处理 React 与 TSX |
@webgpu/types |
为 TypeScript 补充 WebGPU 类型声明 |
@types/react、@types/react-dom |
提供 React 相关类型 |
@types/node |
为 Vite 配置等 Node.js 环境代码提供类型 |
eslint、@eslint/js |
检查常见代码质量问题 |
typescript-eslint |
让 ESLint 理解 TypeScript 语法 |
eslint-plugin-react-hooks |
检查 React Hooks 使用规则 |
eslint-plugin-react-refresh |
检查 React 热更新相关约束 |
globals |
为 ESLint 提供浏览器等环境的全局变量定义 |
Vite 插件配置来自项目现有的 vite.config.ts:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
react(),
tailwindcss()
],
})
react() 让 Vite 处理 React,tailwindcss() 让构建器识别 Tailwind。插件数组体现的是构建阶段的处理链,而不是页面运行后才执行的业务代码。
3. 整体架构:主线程与 Worker 分工
3.1 为什么模型任务不能全部放在 App 中
JavaScript 主线程既要执行代码,也要响应点击、输入、滚动和页面渲染。如果模型下载、Tokenizer 处理和推理持续占用主线程,界面就可能卡顿甚至长时间无响应。
Web Worker 提供独立于主线程的执行环境。它不能直接操作 DOM,但可以处理计算,并通过消息与主线程通信。这非常适合浏览器端 AI:
| 模块 | 主要职责 | 不负责什么 |
|---|---|---|
App.tsx |
显示页面、保存 UI 状态、响应点击、展示错误和进度 | 不直接承担模型推理 |
worker.js |
检查 WebGPU、加载模型、预热模型,未来执行生成任务 | 不直接操作页面 DOM |
| WebGPU | 提供 GPU 计算入口 | 不管理 React 状态和页面组件 |
可以把 App 看成前台,把 Worker 看成后厨。前台不能直接进入后厨执行函数,只能发送一张带有 type 的任务单;后厨完成阶段性工作后,再用带有 status 的消息通知前台。
3.2 两套消息:命令与状态
App 发给 Worker 的消息表示“要做什么”,所以使用 type:
| 命令类型 | 意图 |
|---|---|
check |
检查 WebGPU 和 GPU Adapter |
load |
加载模型并完成预热 |
generate |
生成文本 |
interrupt |
中断生成 |
reset |
重置任务或模型状态 |
Worker 发给 App 的消息表示“做到哪一步”,所以使用 status:
| 状态 | 意图 |
|---|---|
loading |
模型正在加载或预热 |
initiate |
开始处理某个下载文件 |
progress |
文件下载进度发生变化 |
done |
某个文件下载完成 |
ready |
模型可以使用 |
start |
开始生成 |
update |
返回一段流式文本 |
complete |
生成完成 |
error |
发生错误 |
二者共同组成项目的通信协议:
App 创建 Worker
↓
App 发送 { type: "check" }
↓
Worker 执行 check()
↓
Worker 请求 GPU Adapter
↓
检查失败时返回 { status: "error", data: 错误信息 }
↓
App 更新 error 并重新渲染页面
这种结构的意义是 解耦。App 不需要了解模型内部每一步怎样执行,只需要认识状态;Worker 不需要了解页面长什么样,只需要认识命令。
4. App.tsx:主线程如何搭起交互入口
4.1 先做一次快速 WebGPU 检查
进入 App.tsx 后,我们先判断浏览器有没有暴露 WebGPU 入口:
const IS_WEBGPU_AVAILABLE = !!navigator.gpu;
代码刚写下时,部分编辑器会立刻在 gpu 下方标红:
类型“Navigator”上不存在属性“gpu”
这里需要先判断错误来自哪里。浏览器运行 JavaScript 时,会检查 navigator 对象上是否真的存在 gpu;TypeScript 在开发阶段检查的却是 Navigator 接口声明。WebGPU 比较新,当编辑器使用的 TypeScript 或 DOM 类型没有包含这项声明时,就会出现“浏览器可能支持,但类型系统不认识”的情况。
第一种解决方法是使用 as any:
const IS_WEBGPU_AVAILABLE = !!(navigator as any).gpu;
any 是 TypeScript 的任意类型。navigator as any 相当于告诉 TypeScript:“这里暂时不要继续检查 navigator 的属性。”这样可以马上消除红线,适合验证问题是否只是缺少类型声明。
但 any 会同时放弃拼写、参数和返回值检查。如果项目中到处使用 any,类型会沿着变量和函数继续传播,TypeScript 的保护能力也会逐渐消失。因此,这个办法适合临时排查,不适合作为长期方案。
第二种解决方法是安装 WebGPU 类型声明:
pnpm i -D @webgpu/types
-D 表示开发依赖。类型声明只参与编辑器提示和 TypeScript 检查,不会成为浏览器运行模型时需要下载的业务代码。
安装后,在 tsconfig.app.json 的 types 中加入:
"types": ["vite/client", "@webgpu/types"]
完成这一步,TypeScript 就能理解 Navigator.gpu、GPUAdapter 和 GPUDevice。于是代码可以保留清晰的原始写法:
const IS_WEBGPU_AVAILABLE = !!navigator.gpu;
| 解决方式 | 优点 | 代价 | 适用场景 |
|---|---|---|---|
(navigator as any).gpu |
快速、无需安装依赖 | 放弃这一段类型检查 | 临时验证和排查 |
@webgpu/types |
保留完整 WebGPU 类型提示 | 需要安装并配置类型包 | 项目开发的稳定方案 |
navigator.gpu 是 WebGPU 的入口,!! 把它明确转换成布尔值:存在时得到 true,不存在时得到 false。这个判断适合快速决定页面显示哪个分支,但它不能保证一定能拿到可用的 GPU Adapter,所以我们还会在 Worker 中调用 requestAdapter() 做进一步检查。
4.2 用 useRef 保存 Worker,用 useState 保存页面状态
Worker 实例被放进 useRef:
const worker = useRef(null);
useRef 返回一个长期存在的对象,真正的值保存在 worker.current。组件重新渲染时,这个引用不会像普通局部变量一样重新丢失;修改它也不会触发页面渲染。这正符合 Worker 实例的特点:需要长期保存,但创建完成本身不要求页面刷新。
项目中与模型加载有关的状态包括:
const [status, setStatus] = useState(null);
const [error, setError] = useState(null);
const [loadingMessage, setLoadingMessage] = useState("");
const [progressItems, setProgressItems] = useState([]);
const [isRunning, setIsRunning] = useState(false);
沿着模型加载链路,我们先使用 status、error 和 loadingMessage,分别保存加载阶段、错误信息和提示文字。下载列表与生成统计会在相应交互出现时再接入页面。
| 状态 | 目的 |
|---|---|
status |
判断模型处于未加载、加载中还是可用状态 |
error |
保存 Worker 或 WebGPU 返回的错误信息 |
loadingMessage |
保存“正在加载模型”“正在预热”等提示文本 |
useState 与 useRef 最本质的区别是:状态更新会触发 React 重新渲染,引用更新不会。 页面要显示的内容放进 state,外部对象实例放进 ref。
4.3 useEffect 中只创建一次 Worker
创建 Worker 的逻辑位于 useEffect:
useEffect(() => {
if (!worker.current) { // 只实例化一次
// html5 新特性
worker.current = new Worker(new URL("./worker.js", import.meta.url), {
type: "module", // 前端不是默认支持esm
});
// 消息通信
worker.current.postMessage({ type: "check" }); // Do a feature check
}
useEffect(..., []) 表示这段副作用逻辑在组件挂载后执行。创建 Worker 属于 React 渲染之外的操作,所以不放在 JSX 中。
if (!worker.current) 用来避免重复实例化。new URL("./worker.js", import.meta.url) 让 Vite 知道 Worker 文件相对于当前模块的位置,生产构建时 Vite 会单独处理这个资源。type: "module" 表示 Worker 使用 ES Module 模式,Transformers.js 也可以从这个模块环境中接入。
Worker 创建后,App 立即发送:
worker.current.postMessage({ type: "check" });
这不是直接调用 Worker 的 check(),而是向另一个线程投递一条消息。Worker 收到 type: "check" 后,再在自己的环境中调用对应函数。
4.4 接收 loading 与 error 状态
App 通过 message 事件接收 Worker 返回的数据。先处理加载提示和错误这两个直接影响页面的状态:
case "loading":
// Model file start load: add a new progress item to the list.
setStatus("loading");
setLoadingMessage(e.data.data);
break;
case "error":
setError(e.data.data);
break;
当 Worker 返回 loading 时,App 保存加载状态和提示语;当 Worker 返回 error 时,App 保存错误信息。调用这些 setter 后,React 会重新渲染页面。
监听器注册代码为:
worker.current.addEventListener("message", onMessageReceived);
worker.current.addEventListener("error", onErrorReceived);
message 表示 Worker 主动发回的业务消息;error 表示 Worker 脚本自身发生未处理错误。这里先把两个监听入口分开,避免业务失败和 Worker 崩溃混成同一种事件。
4.5 页面如何触发 load 命令
浏览器支持 WebGPU 时,页面会显示加载按钮:
<button
className="border px-4 py-2 rounded-lg bg-blue-400 text-white hover:bg-blue-500 disabled:bg-blue-100 cursor-pointer disabled:cursor-not-allowed select-none"
onClick={() => {
worker.current.postMessage({ type: "load" });
setStatus("loading");
}}
disabled={status !== null || error !== null}
>
Load model
</button>
点击按钮会完成两件事:
- 使用
postMessage({ type: "load" })通知 Worker 加载模型。 - 使用
setStatus("loading")立即更新页面状态。
disabled={status !== null || error !== null} 表示只要进入某个状态,或者出现错误,按钮就不能再次点击。这样可以避免用户连续触发多个模型加载任务。
当浏览器不支持 WebGPU 时,三元表达式会切换到全屏提示页面。也就是说,页面渲染并不负责深度检测 GPU,只负责根据能力判断与状态选择合适的 UI。
5. worker.js:后台线程如何接住任务
5.1 Worker 环境为什么使用 self
Worker 没有页面 DOM,也不能直接使用 document 操作组件。它拥有自己的全局作用域,通常通过 self 注册事件和发送消息:
self.addEventListener("message", async (e) => {
const { type, data } = e.data;
switch (type) {
// 检查webgpu是否支持
case "check":
check();
break;
// 加载模型
case "load":
break;
// 生成文本
case "generate":
break;
// 中断生成
case "interrupt":
break;
// 重置模型
case "reset":
break;
}
});
e.data 就是 App 通过 postMessage 发送的对象。这里先取出 type,再通过 switch 把消息分配到不同函数。我们先让 check 和 load 进入真实函数;讲到生成、中断和重置时,再沿着相同的命令结构填入对应逻辑,不需要把所有任务塞进一个事件回调。
5.2 check:从接口存在到拿到 GPU Adapter
Worker 中的检查函数是:
async function check() {
try {
// window
// DOM Document Object Model document
// BOM Browser Object Model navigator
// adapter 是 GPU 适配器的抽象,
// 后续所有 WebGPU 计算/渲染操作都通过 device 执行
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
// 抛出错误
throw new Error("WebGPU is not supported (no adapter found)");
}
// fp16_supported = adapter.features.has("shader-f16")
} catch (e) {
self.postMessage({
status: "error",
data: e.toString(),
});
}
}
navigator.gpu.requestAdapter() 会异步请求一个 GPUAdapter。Adapter 可以理解为浏览器对可用 GPU 的抽象入口,后续还可以通过它申请 GPUDevice、读取特性与限制。
如果没有合适的 Adapter,结果可能为空,代码主动抛出错误。错误被 catch 捕获后,Worker 不操作页面,而是把错误发回 App:
self.postMessage({
status: "error",
data: e.toString(),
});
这样,硬件检查留在 Worker,用户提示留在 React,职责不会混在一起。
6. TypeScript 配置如何承接 WebGPU 类型
6.1 类型依赖只服务于开发阶段
在第 4 节安装 @webgpu/types 后,浏览器并不会因此获得 WebGPU。这个依赖补充的是 TypeScript 对 WebGPU API 的描述,让编辑器知道 gpu 上有哪些方法、requestAdapter() 返回什么,以及 GPUAdapter、GPUDevice 之间是什么关系。
很多类型包采用 @types/xxx 命名,所以第一次遇到问题时很容易猜成 @types/webgpu。项目实际安装的是 @webgpu/types:
pnpm i -D @webgpu/types
TypeScript 只参与开发和构建检查。Vite 打包后,浏览器执行的是 JavaScript;能否真正使用 WebGPU,仍由浏览器版本、系统、显卡驱动和安全上下文决定。
6.2 tsconfig.json 决定类型检查范围
tsconfig.json 是 TypeScript 项目的配置入口。它决定编译目标、模块解析方式、需要加载的类型、是否生成文件以及检查强度。项目把浏览器代码和 Vite 配置拆成 tsconfig.app.json 与 tsconfig.node.json,再由根配置引用。
| 配置 | 在项目中的作用 |
|---|---|
target: "es2023" |
按较新的 JavaScript 语法目标进行检查 |
lib: ["ES2023", "DOM"] |
加载语言能力和浏览器 DOM 类型 |
types: ["vite/client", "@webgpu/types"] |
加入 Vite 与 WebGPU 类型 |
moduleResolution: "bundler" |
按 Vite 这类打包器的方式解析模块 |
jsx: "react-jsx" |
使用 React JSX 转换方式 |
noEmit: true |
TypeScript 负责检查,不直接输出 JS,由 Vite 完成构建 |
include: ["src"] |
检查 src 下的应用源码 |
strictNullChecks: false |
允许较宽松的 null 类型处理 |
noImplicitAny: false |
允许部分参数被推断为 any |
这套配置先保证框架推进时能够识别 WebGPU。等 App 与 Worker 的消息结构完全稳定,再为 type、status 和 data 定义明确类型,逐步提高检查强度。
TypeScript 不会增强浏览器的运行能力,它增强的是开发阶段的可理解性和错误发现能力。
7. 把整个框架重新串起来
7.1 页面启动后的真实执行顺序
把前面的代码连起来,页面启动后的执行流程如下:
main.tsx创建 React 根节点并渲染App。App.tsx通过navigator.gpu做快速能力判断。- 组件挂载后,
useEffect创建模块化 Worker。 - App 发送
{ type: "check" }。 - Worker 匹配
case "check"并执行requestAdapter()。 - 检查失败时,Worker 返回
{ status: "error" }。 - App 调用
setError(),React 重新渲染错误提示。 - 检查通过后,用户可以点击
Load model。 - App 发送
{ type: "load" },Worker 在case "load"接住命令。 - 执行模型加载的
load()从下一节开始实现,这里先停在消息入口。
这条链路中最重要的不是某一个 API,而是 跨线程状态流:
用户操作
↓
React 事件
↓
App postMessage(type)
↓
Worker switch(type)
↓
WebGPU / 模型任务
↓
Worker postMessage(status)
↓
App switch(status)
↓
setState
↓
React 更新页面
7.2 App.tsx 框架完整版
前面按照 WebGPU 判断、Worker 创建、消息监听和按钮交互逐步写代码,合在一起就是下面的 App.tsx。为了让本篇主线更集中,示例问题、滚动引用、Token 统计等暂时不参与交互的变量没有放进这份汇总代码;下面每一段都能在前文找到对应解释。
import { useEffect, useState, useRef } from "react";
// 快速判断浏览器是否暴露 WebGPU 入口。
// @webgpu/types 让 TypeScript 能识别 navigator.gpu。
const IS_WEBGPU_AVAILABLE = !!navigator.gpu;
function App() {
// 保存 Worker 实例。修改 ref 不会触发 React 重新渲染。
const worker = useRef(null);
// 保存模型加载状态、错误信息和 Worker 返回的加载文案。
const [status, setStatus] = useState(null);
const [error, setError] = useState(null);
const [loadingMessage, setLoadingMessage] = useState("");
useEffect(() => {
// Worker 只创建一次,避免组件渲染时反复创建后台线程。
if (!worker.current) {
worker.current = new Worker(new URL("./worker.js", import.meta.url), {
// 使用模块化 Worker,为导入 Transformers.js 做准备。
type: "module",
});
// App 不直接调用 check(),而是给 Worker 发送 check 命令。
worker.current.postMessage({ type: "check" });
}
// 接收 Worker 主动返回的业务状态。
const onMessageReceived = (e) => {
switch (e.data.status) {
case "loading":
// 保存加载阶段与提示文字,触发 React 重新渲染。
setStatus("loading");
setLoadingMessage(e.data.data);
break;
case "initiate":
break;
case "progress":
break;
case "done":
break;
case "ready":
break;
case "start":
break;
case "update":
break;
case "complete":
break;
case "error":
// Worker 主动上报业务错误时,把错误保存进 state。
setError(e.data.data);
break;
}
};
// 这个监听器对应 Worker 脚本本身的运行错误。
const onErrorReceived = (e) => {
};
worker.current.addEventListener("message", onMessageReceived);
worker.current.addEventListener("error", onErrorReceived);
}, []);
return (
IS_WEBGPU_AVAILABLE ? (
<div className="flex flex-col h-screen mx-auto items justify-end text-gray-800 dark:text-gray-200 bg-white dark:bg-gray-900">
{/* error 有内容时才渲染错误提示。 */}
{error && (
<div className="text-red-500 text-center mb-2">
<p className="mb-1">
Unable to load model due to the following error:
</p>
<p className="text-sm">{error}</p>
</div>
)}
<button
className="border px-4 py-2 rounded-lg bg-blue-400 text-white hover:bg-blue-500 disabled:bg-blue-100 cursor-pointer disabled:cursor-not-allowed select-none"
onClick={() => {
// 点击后通知 Worker 进入模型加载流程。
worker.current.postMessage({ type: "load" });
setStatus("loading");
}}
// 进入加载状态或出现错误后,禁止重复点击。
disabled={status !== null || error !== null}
>
Load model
</button>
</div>
) : (
// 浏览器没有暴露 navigator.gpu 时显示全屏提示。
<div className="fixed w-screen h-screen bg-black z-10 bg-opacity-[92%] text-white text-2xl font-semibold flex justify-center items-center text-center">
WebGPU is not supported
<br />
by this browser :(
</div>
)
);
}
export default App;
App.tsx 的主线只有三件事:创建 Worker、发送命令、把 Worker 状态变成 React 状态。loadingMessage 在消息到达时被保存,等加载进度界面讲到它时,再把这段状态渲染到 JSX;这里不提前加入新的 UI。
7.3 worker.js 框架完整版
worker.js 与 App 保持相反方向的职责:它接收 type 命令,在这一节执行 WebGPU 检查,再通过 status 把错误信息发回页面。load 命令只保留消息入口,不在这里展开模型加载。
// Worker 没有 DOM,不能直接操作页面。
// 它通过 self 接收和发送消息,通过 navigator 访问 WebGPU。
async function check() {
try {
// 请求 GPU Adapter,它是浏览器对可用 GPU 的抽象入口。
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error("WebGPU is not supported (no adapter found)");
}
// 可以继续通过 adapter.features 检查 shader-f16 等能力。
// fp16_supported = adapter.features.has("shader-f16")
} catch (e) {
// Worker 不操作 React 页面,只把错误信息发回 App。
self.postMessage({
status: "error",
data: e.toString(),
});
}
}
// Worker 统一监听 App 发来的命令。
self.addEventListener("message", async (e) => {
const { type, data } = e.data;
switch (type) {
case "check":
// 检查 WebGPU。
check();
break;
case "load":
// 下一节从这里进入模型加载。
break;
case "generate":
// 文本生成命令沿用同一套消息协议。
break;
case "interrupt":
// 中断生成命令沿用同一套消息协议。
break;
case "reset":
// 重置命令沿用同一套消息协议。
break;
}
});
把两份代码并排看,交互关系会非常清楚:App.tsx 使用 worker.current.postMessage() 发送 check 或 load,worker.js 使用 switch(type) 接住命令。这一节让 check 进入函数并在失败时返回 error;load 到达对应 case 后暂停,下一节再让它进入模型加载函数。
这就是浏览器端模型应用最重要的一条骨架:UI 事件变成 Worker 命令,Worker 任务变成状态消息,状态消息再变成 React 页面变化。
总结
这篇文章从模型为什么能进入浏览器讲起,顺着 Hugging Face、Transformers.js、浏览器缓存和 WebGPU 串起端侧推理链路。写下 const IS_WEBGPU_AVAILABLE = !!navigator.gpu 时,我们先遇到 TypeScript 不认识 gpu 的问题,再比较 navigator as any 与 @webgpu/types 两种解法:前者适合临时绕过检查,后者通过正式类型声明保留编辑器提示,因此项目选择安装依赖并在 tsconfig.app.json 中加入 WebGPU 类型。
接着,App.tsx 使用 useRef 保存 Worker,使用 useState 保存页面状态,通过 postMessage 发送 check、load 命令;worker.js 使用 switch(type) 分发任务,通过 requestAdapter() 检查 GPU,并在失败时把 error 发回 React。文章末尾的两份完整代码把这条交互链集中呈现:App 管界面与状态,Worker 管后台任务,WebGPU 提供 GPU 入口。
至此,系列第一部分把模型应用的运行边界和通信方式建立起来。下一部分会从 case "load" 继续,实现 Worker 中的 load(),再进入 Transformers.js、模型 ID、Tokenizer、下载进度和浏览器缓存。
更多推荐



所有评论(0)