欢迎光临
我们一直在努力

WebGPU DeepSeek项目实战(一):从Hugging Face模型链路到Web Worker通信框架

WebGPU DeepSeek项目实战(一):从Hugging Face模型链路到Web Worker通信框架

  • 前言
  • 1. 项目要解决什么问题
    • 1.1 为什么尝试在浏览器中运行模型
    • 1.2 Hugging Face、ModelScope 与模型社区
    • 1.3 Transformers.js 在链路中的位置
  • 2. 项目依赖与工程工具
    • 2.1 运行时依赖分别负责什么
    • 2.2 开发依赖与 TypeScript 工具链
  • 3. 整体架构:主线程与 Worker 分工
    • 3.1 为什么模型任务不能全部放在 App 中
    • 3.2 两套消息:命令与状态
  • 4. App.tsx:主线程如何搭起交互入口
    • 4.1 先做一次快速 WebGPU 检查
    • 4.2 用 useRef 保存 Worker,用 useState 保存页面状态
    • 4.3 useEffect 中只创建一次 Worker
    • 4.4 接收 loading 与 error 状态
    • 4.5 页面如何触发 load 命令
  • 5. worker.js:后台线程如何接住任务
    • 5.1 Worker 环境为什么使用 self
    • 5.2 check:从接口存在到拿到 GPU Adapter
  • 6. TypeScript 配置如何承接 WebGPU 类型
    • 6.1 类型依赖只服务于开发阶段
    • 6.2 tsconfig.json 决定类型检查范围
  • 7. 把整个框架重新串起来
    • 7.1 页面启动后的真实执行顺序
    • 7.2 App.tsx 框架完整版
    • 7.3 worker.js 框架完整版
  • 总结

前言

最近我在做一个名为 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 :&#40;
    </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)
    未经允许不得转载:171主机测评 » WebGPU DeepSeek项目实战(一):从Hugging Face模型链路到Web Worker通信框架
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址