浏览器也能跑大模型:WebGPU + Transformers.js 本地运行 DeepSeek-R1

浏览器也能跑大模型:WebGPU + Transformers.js 本地运行 DeepSeek-R1

借助 WebGPU 加速和 Transformers.js,在浏览器中零后端部署运行 15 亿参数的推理模型。

前言

大模型离前端远吗?以前可能隔着服务端 API、GPU 集群、按 token 计费------但现在已经不一样了。这篇文章带你从 0 搭建一个完全在浏览器里跑的 DeepSeek-R1 推理应用:模型下载到本地、WebGPU 加速推理、全程数据不出浏览器。

适合读者:对 AI 感兴趣的前端开发者,有 React 基础即可。

读完你会收获:

  • WebGPU 在浏览器中的实际应用场景
  • Transformers.js 如何下载、加载并运行 ONNX 格式的 LLM
  • 为什么 Web Worker + 单例模式是本地推理的最佳拍档
  • TypeScript 对实验性浏览器 API 的类型处理

文末附有完整可运行项目代码。

项目概览

一句话:在浏览器中用 WebGPU 运行 DeepSeek-R1-Distill-Qwen-1.5B 模型,实现本地 NLP 对话推理。

数据链路

css 复制代码
HuggingFace 模型仓库
  → Transformers.js 远程下载(首次 ~1.5GB)
    → 浏览器 IndexedDB 缓存
      → ONNX Runtime Web + WebGPU 本地推理
        → 流式输出 Markdown → marked 转 HTML → 页面渲染

项目文件树

ruby 复制代码
deepseek-r1-webgpu/
├── index.html                 # 入口
├── package.json               # 依赖:React 19 + Vite + TailwindCSS 4
├── vite.config.ts             # Vite 配置
├── src/
│   ├── main.tsx               # React 挂载点
│   ├── App.tsx                # 主界面 + WebGPU 检测 + 模型加载状态
│   ├── App.css                # 样式(TailwindCSS 下基本只是辅助)
│   ├── index.css              # 全局:@import "tailwindcss"
│   └── worker.js              # Web Worker:模型加载 + 推理(核心)
└── public/
    └── logo.png               # DeepSeek Logo

核心技术栈

技术 作用
@huggingface/transformers 浏览器端模型加载与推理
WebGPU 调用本地 GPU 加速矩阵运算
Web Worker 模型推理在独立线程,不冻结 UI
Singleton Pattern 模型实例全局唯一,避免重复加载
marked AI 返回的 Markdown 转 HTML 渲染
@webgpu/types TypeScript 对 navigator.gpu 的类型声明
React 19 + Vite 8 + TailwindCSS 4 界面层

依赖与版本

json 复制代码
{
  "@huggingface/transformers": "3.7.1",
  "@tailwindcss/vite": "^4.3.3",
  "marked": "^15.0.5",
  "react": "^19.2.7",
  "react-dom": "^19.2.7",
  "tailwindcss": "^4.3.3",
  "@webgpu/types": "^0.1.71"
}

核心知识点一:Web Worker 为什么要独立线程?

问题

大模型推理是计算密集型操作------一次前向传播涉及数十亿次矩阵运算。如果放在主线程:

markdown 复制代码
用户在输入框打字
  → 刚好撞上模型推理
    → 60fps → 0fps
      → 页面卡死 💀

解决方案

Web Worker 在独立线程跑模型,和 UI 线程互不干扰:

js 复制代码
// App.tsx - 主线程创建 Worker
const worker = useRef(null);

useEffect(() => {
  if (!worker.current) {
    worker.current = new Worker(
      new URL("./worker.js", import.meta.url),
      { type: "module" }  // Worker 内支持 ESM import
    );
    worker.current.postMessage({ type: "check" }); // 检测 WebGPU
  }
}, []);
js 复制代码
// worker.js - 独立线程中跑
self.addEventListener("message", async (e) => {
  const { type } = e.data;
  switch (type) {
    case "check":  check();  break;  // 检测 WebGPU 支持
    case "load":   load();   break;  // 下载并加载模型
    case "generate":         break;  // 推理生成
  }
});

通信机制:postMessage

lua 复制代码
主线程                          Worker 线程
  │                                │
  │── postMessage({type:"load"})──→│  触发模型加载
  │                                │
  │←── postMessage({status:"loading"},"正在下载...")──┤
  │←── postMessage({status:"progress"}, 45%) ──────┤
  │←── postMessage({status:"ready"}) ──────────────┤

postMessage 是浏览器原生 API,数据通过结构化克隆传递,无需引入任何第三方消息库。

核心知识点二:Singleton 单例模式------模型只加载一次

为什么必须用单例?

DeepSeek-R1-Distill-Qwen-1.5B 的 ONNX 权重文件约 1.5GB。如果意外调用了两次 load()

  • 下载 2 × 1.5GB = 3GB 流量
  • GPU 显存分配 2 份 = 浏览器 OOM 崩溃
  • 磁盘缓存冗余

模型是你全局唯一的稀缺资源,Singleton 是最自然的抽象。

代码实现

js 复制代码
class TextGenerationPipeline {
  static model_id = "onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX";

  static async getInstance(progress_callback = null) {
    // ??= 含义:如果 this.tokenizer 为 null/undefined,则执行右侧赋值
    // 第二次调用 getInstance() 时,直接跳过,返回已缓存的实例
    this.tokenizer ??= AutoTokenizer.from_pretrained(this.model_id, {
      progress_callback,  // 下载进度回调 → 实时更新 UI
    });

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

逐行拆解

代码 作用
static model_id 模型标识,挂在类上而非实例上,全局唯一
static getInstance() 工厂方法,不管调几次,保证只创建一个实例
this.tokenizer ??= 逻辑空赋值 :第二次调用时 this.tokenizer 已有值,右侧的 from_pretrained 不执行
progress_callback 下载进度(如 45%)回调给主线程更新 UI
Promise.all([...]) 等待 tokenizer 就绪;后续可扩展加载其他组件(如生成器)

为什么不是普通变量?

js 复制代码
// ❌ 坏做法:每次 import worker.js 都执行
const tokenizer = await AutoTokenizer.from_pretrained(...);

// ❌ 坏做法:全局变量,无封装
let _tokenizer = null;
function getTokenizer() {
  if (!_tokenizer) _tokenizer = await AutoTokenizer.from_pretrained(...);
  return _tokenizer;
}

// ✅ 做法:用 static 挂类上,语义清晰
class TextGenerationPipeline {
  static tokenizer = null;
  static async getInstance() {
    this.tokenizer ??= AutoTokenizer.from_pretrained(...);
    return this.tokenizer;
  }
}

类的 static 属性天然表达"属于这个类型,不属于某个实例"的语义,比裸变量更内聚;后续添加其他模型组件(如生成器)时,统一通过 getInstance 管理。

核心知识点三:WebGPU 检测与 TypeScript 类型处理

问题

navigator.gpu 是 Chrome 113+ 才引入的实验性 API。直接写代码:

ts 复制代码
// ❌ TypeScript 报错:Property 'gpu' does not exist on type 'Navigator'
const IS_WEBGPU_AVAILABLE = !!navigator.gpu;

TS 的类型定义里 Navigator 没有 gpu 这个属性,编译都过不了。

两种解法

快速绕过(不推荐)

ts 复制代码
// as any 忽略类型检查------能用,但污染了其他地方的代码
!!(navigator as any).gpu

根本解决(项目采用)

bash 复制代码
pnpm i -D @webgpu/types

然后在 tsconfig.app.json 中配置:

json 复制代码
{
  "compilerOptions": {
    "types": ["@webgpu/types"]  // TypeScript 认识 navigator.gpu 了
  }
}

安装后,TS 自动获得完整的 WebGPU 类型提示:

ts 复制代码
// ✅ 完美工作,有代码补全
const adapter: GPUAdapter = await navigator.gpu.requestAdapter();

Worker 中的检测逻辑

js 复制代码
async function check() {
  const adapter = await navigator.gpu.requestAdapter();
  if (!adapter) {
    throw new Error("WebGPU is not supported (no adapter found)");
  }
  self.postMessage({ status: "ready" });
}

requestAdapter() 请求 GPU 访问权限:返回 null 说明浏览器不支持或驱动不可用;返回 adapter 对象说明环境就绪。

逐组件拆解

App.tsx ------ 主界面与状态机

App.tsx 有三个职责:

1. Web Worker 生命周期管理

ts 复制代码
useEffect(() => {
  worker.current = new Worker(
    new URL("./worker.js", import.meta.url),
    { type: "module" }
  );
  worker.current.postMessage({ type: "check" }); // 启动即检测

  worker.current.addEventListener("message", onMessageReceived);
  return () => worker.current.removeEventListener("message", onMessageReceived);
}, []);

useRef 存 Worker 引用,确保整个组件生命周期只有一个 Worker 实例。首次渲染时创建、注册消息监听。

2. 状态机驱动 UI

onMessageReceivede.data.status 分派:

status UI 行为
loading 显示加载文案(如"正在下载 tokenizer...")
initiate 开始下载某个模型文件
progress 更新进度条
done 单个文件下载完成
ready 所有文件就绪,可开始对话
start 推理开始
update 流式输出新 token
complete 生成结束

每个状态对应不同的 UI 展示,构成一个清晰的状态机。

3. WebGPU 兜底提示

ts 复制代码
const IS_WEBGPU_AVAILABLE = !!navigator.gpu;

return IS_WEBGPU_AVAILABLE
  ? (/* 正常界面 */)
  : (
    <div className="fixed w-screen h-screen ...">
      WebGPU is not supported by this browser :(
    </div>
  );

WebGPU 不可用时直接全屏提示,不展示加载按钮。

worker.js ------ 模型推理核心

js 复制代码
async function load() {
  self.postMessage({ status: "loading", data: "Loading model..." });

  const [tokenizer] = await TextGenerationPipeline.getInstance((x) => {
    self.postMessage(x); // 进度回调 → 主线程
  });

  self.postMessage({ status: "ready" });
}

核心流程:

scss 复制代码
load() 被调用
  → postMessage("loading") 通知 UI
    → TextGenerationPipeline.getInstance(进度回调)
      → AutoTokenizer.from_pretrained(model_id, { progress_callback })
        → 下载模型文件(每次进度更新回调 postMessage)
          → 分词器创建完成
            → postMessage("ready") 通知 UI

为什么 textarea 要用 ref 取值?

CommentBox.jsx 中,<textarea> 没有绑定 value + onChange,而是通过 useRef 在提交时取值:

jsx 复制代码
const textareaRef = useRef(null);
const handleSubmit = () => {
  const comment = textareaRef.current.value; // 提交时才读
};

这是非受控组件模式。选择它的原因:用户输入过程中不需要实时校验或格式化文本,因此没必要每次按键都触发 state 更新和重渲染。提交时一次性读取即可------性能最优。

总结

回顾你从这篇文章中收获的:

  1. WebGPU 不是框架专属 --- 浏览器原生 navigator.gpu 就能用,配合 ONNX Runtime Web 跑大模型
  2. Web Worker 是大模型前端的标配 --- 计算密集型任务必须移出主线程,否则 UI 卡死
  3. Singleton 是模型加载的最优解 --- 1.5GB 权重的实例绝不能重复创建
  4. 实验性 API 的类型处理 --- as any 只是临时方案,正解是安装 @webgpu/types
  5. 架构先于代码 --- 主线程/Worker 通信、单例管理、状态机驱动 UI,这些设计决策比写代码更重要

下一步可以扩展的方向:补全 generate 分支实现流式推理对话、增加对话历史管理、优化首包延迟。

你觉得在浏览器端跑大模型,最大的痛点是什么?是首包加载速度、显存占用,还是模型精度?欢迎在评论区交流 👏

完整项目代码

项目地址:Gitee 仓库

bash 复制代码
# 1. 克隆仓库
git clone git@gitee.com:dcx2758/ai_doubao_dcx.git

# 2. 进入项目目录
cd ai_doubao_dcx/ai/webgpu-deepseek/deepseek-r1-webgpu

# 3. 安装依赖(pnpm 或 npm 均可)
pnpm install
# 或
npm install

# 4. 启动开发服务器
pnpm run dev
# 或
npm run dev

# 5. 浏览器打开 http://localhost:5173
# 需要 Chrome 113+ / Edge 113+,WebGPU 默认开启
相关推荐
用户33144195556732 小时前
Rush Monorepo 构建缓存指南
前端
物联网软硬件开发-轨物科技2 小时前
【轨物方案】从五维感知到一键顺控:箱变智能化不是一个传感器能解决的事
人工智能·科技·其他·机器人·开源
windliang2 小时前
Claude Code 源码分析(八):Memory 如何被写入、整理与按需召回
前端·算法·面试
木公子2 小时前
Vue3源码精读03:响应式核心依赖追踪机制|track与trigger底层源码全解析
前端·vue.js
睡觉时不困4422 小时前
Obsidian 三端同步完整流程:电脑、手机、平板通过 Gitee 实时同步
前端
Swift社区2 小时前
Python 开发环境怎么选?PyCharm、VS Code、Trae 谁更适合 AI 开发?
人工智能·python·pycharm
渣波3 小时前
React Hooks 核心基石:深度解析 `useState` 的类型推断、泛型约束与空值安全
前端·typescript
SLD_Allen3 小时前
Kubernetes + Ray + Volcano:云原生AI训练调度体系
人工智能·云原生·kubernetes
YIAN3 小时前
吃透这 5 个核心点,你的 React+TS 代码直接上一个台阶
前端·react.js·typescript