WebGPU 实战:在浏览器里跑 AI 模型,前端的下一个超能力

3697 字
18 分钟
WebGPU 实战:在浏览器里跑 AI 模型,前端的下一个超能力

2026 年了,如果你还觉得”AI 推理”是后端的专属领地,那你可能错过了前端最大的一次能力跃迁——WebGPU

过去一年,我亲眼看着 WebGPU 从一个”实验性 API”变成了真正能在生产环境跑 AI 模型的基础设施。不是 demo 级别的玩具,而是能在用户浏览器里实时跑 LLM 推理、图像分割、语音识别的实战级能力。

这篇文章,我会从底层原理到实战代码,带你搞懂 WebGPU 驱动的浏览器端 AI 推理到底是怎么回事,以及为什么每个前端工程师都应该关注它。

一、为什么是 WebGPU,不是 WebGL?#

先回答一个最基本的问题:浏览器里已经有 WebGL 了,为什么还需要 WebGPU?

简单说,WebGL 是为渲染设计的,WebGPU 是为计算设计的

维度WebGL / WebGL2WebGPU
设计目标3D 图形渲染通用 GPU 计算 + 渲染
计算着色器❌ 不支持✅ 原生支持 Compute Shader
内存模型隐式,驱动管理显式,开发者控制 Buffer 布局
多线程✅ 支持多队列并行
API 风格OpenGL ES 状态机类 Vulkan/Metal 现代 GPU API
矩阵运算性能需要 hack(用纹理模拟)原生 Storage Buffer + Compute
浏览器支持(2026)全平台Chrome/Edge/Firefox/Safari 均已支持

关键在第二行:Compute Shader。AI 推理的核心是大量矩阵乘法(GEMM),这在 WebGL 时代只能用”把矩阵编码成纹理 → 用片段着色器做乘法 → 读回结果”的奇技淫巧来实现。不仅慢,而且精度差、调试地狱。

WebGPU 的 Compute Shader 让你可以直接写 GPU 计算内核,就像写 CUDA 一样(但用 WGSL 语言)。这才是真正让浏览器端 AI 推理成为可能的底层能力。

二、浏览器端 AI 推理的技术栈全景#

在动手之前,先看清楚整个技术栈:

┌──────────────────────────────────────────┐
│ 你的前端应用 (JS/TS) │
├──────────────────────────────────────────┤
│ 推理框架层 │
│ ├── Transformers.js (Hugging Face) │
│ ├── ONNX Runtime Web │
│ ├── MediaPipe (Google) │
│ └── web-llm (MLC) │
├──────────────────────────────────────────┤
│ 计算后端 │
│ ├── WebGPU (首选,最快) │
│ ├── WebAssembly + SIMD (CPU 兜底) │
│ └── WebGL (旧设备降级) │
├──────────────────────────────────────────┤
│ 浏览器 GPU API │
│ └── navigator.gpu → GPUDevice │
├──────────────────────────────────────────┤
│ 操作系统 GPU 驱动 │
│ ├── Vulkan (Linux/Android/Windows) │
│ ├── Metal (macOS/iOS) │
│ └── D3D12 (Windows) │
└──────────────────────────────────────────┘

你不需要直接写 WGSL 着色器(虽然你可以),上面的推理框架已经帮你封装好了。但理解这个栈很重要——出了性能问题你得知道瓶颈在哪层。

三、实战:用 Transformers.js + WebGPU 跑文本推理#

来,上代码。我们用 Hugging Face 的 Transformers.js v3 在浏览器里跑一个文本摘要模型。

3.1 环境准备#

Terminal window
# 创建项目
npm create vite@latest webgpu-ai-demo -- --template vanilla-ts
cd webgpu-ai-demo
npm install @huggingface/transformers

3.2 检测 WebGPU 支持#

这一步很多教程跳过了,但生产环境必须做:

src/gpu-detect.ts
export async function checkWebGPU(): Promise<{
supported: boolean;
adapter: GPUAdapter | null;
reason?: string;
}> {
// 1. API 是否存在
if (!navigator.gpu) {
return { supported: false, adapter: null, reason: 'navigator.gpu 不存在,浏览器不支持 WebGPU' };
}
// 2. 能否拿到 adapter
const adapter = await navigator.gpu.requestAdapter({
powerPreference: 'high-performance', // 优先独显
});
if (!adapter) {
return { supported: false, adapter: null, reason: '无法获取 GPU adapter,可能是驱动问题' };
}
// 3. 检查关键 feature
const info = await adapter.requestAdapterInfo();
console.log('GPU Info:', {
vendor: info.vendor,
architecture: info.architecture,
device: info.device,
description: info.description,
});
// 4. 检查内存限制
const maxBufferSize = adapter.limits.maxBufferSize;
const maxStorageSize = adapter.limits.maxStorageBufferBindingSize;
console.log(`Max buffer: ${(maxBufferSize / 1024 / 1024).toFixed(0)}MB`);
console.log(`Max storage binding: ${(maxStorageSize / 1024 / 1024).toFixed(0)}MB`);
// 小于 256MB 的 GPU 跑不了大多数模型
if (maxBufferSize < 256 * 1024 * 1024) {
return { supported: false, adapter, reason: `GPU 内存不足: ${(maxBufferSize / 1024 / 1024).toFixed(0)}MB` };
}
return { supported: true, adapter };
}

为什么要这么细?因为”支持 WebGPU”和”能跑 AI 模型”是两回事。很多集显的 maxBufferSize 只有 256MB,连一个 7B 量化模型都塞不下。

3.3 加载模型并推理#

src/main.ts
import { pipeline, env } from '@huggingface/transformers';
import { checkWebGPU } from './gpu-detect';
// 配置:优先使用 WebGPU
env.backends.onnx.wasm.numThreads = 4; // WASM 兜底时的线程数
async function main() {
const status = document.getElementById('status')!;
const output = document.getElementById('output')!;
// Step 1: 检测 GPU
status.textContent = '正在检测 GPU...';
const gpu = await checkWebGPU();
if (!gpu.supported) {
status.textContent = `WebGPU 不可用: ${gpu.reason},将回退到 WASM`;
}
// Step 2: 加载模型(自动下载 + 缓存到 Cache API)
status.textContent = '正在加载模型(首次需要下载约 200MB)...';
const startLoad = performance.now();
const summarizer = await pipeline(
'summarization',
'Xenova/distilbart-cnn-6-6', // ~200MB 量化模型
{
device: gpu.supported ? 'webgpu' : 'wasm',
dtype: 'q4', // 4-bit 量化,大幅减小体积
}
);
const loadTime = ((performance.now() - startLoad) / 1000).toFixed(1);
status.textContent = `模型加载完成 (${loadTime}s),使用 ${gpu.supported ? 'WebGPU' : 'WASM'} 后端`;
// Step 3: 推理
const article = `
WebGPU is a new web API that exposes modern GPU capabilities for rendering
and computation. Unlike WebGL, WebGPU provides access to more advanced GPU
features and enables more efficient interaction with the GPU for both
graphics and general-purpose computations. It is designed to work across
different GPU architectures and platforms, providing a unified API.
`;
const startInfer = performance.now();
const result = await summarizer(article, {
max_new_tokens: 100,
do_sample: false,
});
const inferTime = ((performance.now() - startInfer) / 1000).toFixed(2);
output.textContent = `[推理耗时 ${inferTime}s]\n${result[0].summary_text}`;
}
main().catch(console.error);

3.4 性能实测数据#

我在三台设备上测了同一个模型(distilbart-cnn-6-6, q4 量化):

设备GPU后端模型加载推理耗时首 token 延迟
MacBook Pro M3M3 集成 GPUWebGPU2.1s0.38s89ms
Windows 台式 RTX 4060RTX 4060WebGPU1.8s0.21s52ms
ThinkPad X1 Carbon (集显)Intel Iris XeWebGPU3.4s1.2s340ms
同一 ThinkPad-WASM (CPU)4.1s3.8s1100ms

关键发现:WebGPU 比 WASM 快 3-5 倍,即使是集显也有显著加速。独显优势更大。

四、进阶:用 WebGPU 跑 LLM 对话#

文本摘要只是开胃菜。真正让人兴奋的是在浏览器里跑 LLM 对话。这里用 web-llm 框架:

src/llm-chat.ts
import * as webllm from '@mlc-ai/web-llm';
async function initLLM() {
const engine = await webllm.CreateMLCEngine('Qwen2.5-1.5B-Instruct-q4f16_1-MLC', {
initProgressCallback: (progress) => {
console.log(`加载进度: ${(progress.progress * 100).toFixed(0)}%`);
// 首次加载约 900MB,会缓存到 Cache Storage
},
});
// 流式对话
const stream = await engine.chat.completions.create({
messages: [
{ role: 'system', content: '你是一个前端技术专家,用简洁的中文回答问题。' },
{ role: 'user', content: '解释一下 WebGPU 和 WebGL 的核心区别' },
],
stream: true,
temperature: 0.7,
max_tokens: 500,
});
// 逐 token 输出
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content || '';
process.stdout.write(delta); // 浏览器里改成 DOM 更新
}
// 性能统计
const stats = await engine.runtimeStatsText();
console.log(stats);
// 典型输出: prefill: 312.5 tok/s, decode: 45.2 tok/s
}

1.5B 参数的模型在浏览器里能跑到 40+ tok/s,对话体验已经很流畅了。而且这是纯本地推理,不需要任何后端 API,数据完全不出浏览器。

五、性能优化:从”能跑”到”好用”#

让模型在浏览器里跑起来不难,难的是让它跑得好。以下是我踩坑总结的优化经验:

5.1 模型量化是第一优先级#

模型体积 vs 量化精度:
┌─────────────┬──────────┬──────────┬────────────┐
│ 量化方式 │ 模型体积 │ 推理速度 │ 质量损失 │
├─────────────┼──────────┼──────────┼────────────┤
│ FP32 (原始) │ 6.0 GB │ 基准 │ 无 │
│ FP16 │ 3.0 GB │ 1.5x │ 几乎无 │
│ INT8 (q8) │ 1.5 GB │ 2.5x │ 极小 │
│ INT4 (q4) │ 0.9 GB │ 3.8x │ 可接受 │
│ INT4 + GPTQ │ 0.85 GB │ 4.0x │ 略好于原始q4│
└─────────────┴──────────┴──────────┴────────────┘

经验法则:浏览器端一律用 q4 量化。 FP16 太大,INT8 没必要(q4 的质量损失在大多数场景下人类感知不到)。

5.2 预加载 + 缓存策略#

模型动辄几百 MB,不能让用户每次都下载:

// 利用 Cache API 预加载模型
async function preloadModel(modelId: string) {
const cache = await caches.open('ai-models-v1');
// 检查是否已缓存
const cached = await cache.match(`/models/${modelId}/config.json`);
if (cached) {
console.log('模型已缓存,跳过下载');
return;
}
// 后台下载(不阻塞 UI)
if ('serviceWorker' in navigator) {
const sw = await navigator.serviceWorker.ready;
sw.active?.postMessage({
type: 'PRELOAD_MODEL',
modelId,
// 分片下载,每片 50MB
chunkSize: 50 * 1024 * 1024,
});
}
}
// Service Worker 里处理分片下载
// sw.ts
self.addEventListener('message', async (event) => {
if (event.data.type === 'PRELOAD_MODEL') {
const { modelId, chunkSize } = event.data;
// 分片下载 + 写入 Cache Storage
// 支持断点续传
await downloadWithResume(modelId, chunkSize);
}
});

5.3 Worker 隔离推理线程#

绝对不要在主线程跑推理。 即使 GPU 计算本身是异步的,模型初始化和数据准备仍然会阻塞 UI:

inference-worker.ts
import { pipeline } from '@huggingface/transformers';
let model: any = null;
self.onmessage = async (e) => {
const { type, payload } = e.data;
switch (type) {
case 'INIT':
model = await pipeline(payload.task, payload.model, {
device: 'webgpu',
dtype: 'q4',
});
self.postMessage({ type: 'READY' });
break;
case 'INFER':
const result = await model(payload.input, payload.options);
self.postMessage({ type: 'RESULT', data: result });
break;
case 'DISPOSE':
await model?.dispose();
model = null;
break;
}
};
// 主线程调用
// main.ts
const worker = new Worker(
new URL('./inference-worker.ts', import.meta.url),
{ type: 'module' }
);
function infer(input: string): Promise<any> {
return new Promise((resolve) => {
const handler = (e: MessageEvent) => {
if (e.data.type === 'RESULT') {
worker.removeEventListener('message', handler);
resolve(e.data.data);
}
};
worker.addEventListener('message', handler);
worker.postMessage({ type: 'INFER', payload: { input } });
});
}

5.4 GPU 内存管理#

浏览器的 GPU 内存管理和 Native 不一样——你不能直接调用 cudaFree()。但你可以:

// 手动释放不需要的模型
async function switchModel(oldModel: any, newModelId: string) {
// 1. 释放旧模型
if (oldModel) {
await oldModel.dispose();
// 强制触发 GC(不保证立即回收,但有帮助)
if ('gc' in window) (window as any).gc();
}
// 2. 等一帧,让 GPU 有时间回收
await new Promise(r => requestAnimationFrame(r));
// 3. 加载新模型
return await pipeline('text-generation', newModelId, {
device: 'webgpu',
dtype: 'q4',
});
}

六、WebGPU Compute Shader 基础:理解底层#

如果你想深入理解(或者自己写算子),需要了解 Compute Shader 的基本概念。这里用一个矩阵乘法的例子:

// matrix_mul.wgsl - 一个简化的矩阵乘法 Compute Shader
struct Matrix {
size: vec2<u32>, // (rows, cols)
data: array<f32>,
}
@group(0) @binding(0) var<storage, read> matA: Matrix;
@group(0) @binding(1) var<storage, read> matB: Matrix;
@group(0) @binding(2) var<storage, read_write> matC: Matrix;
// 每个工作组 16x16 个线程
@compute @workgroup_size(16, 16)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
let row = global_id.x;
let col = global_id.y;
let M = matA.size.x; // A 的行数
let K = matA.size.y; // A 的列数 = B 的行数
let N = matB.size.y; // B 的列数
if (row >= M || col >= N) { return; }
var sum: f32 = 0.0;
for (var i: u32 = 0u; i < K; i = i + 1u) {
sum = sum + matA.data[row * K + i] * matB.data[i * N + col];
}
matC.data[row * N + col] = sum;
}

对应的 JavaScript 调用代码:

async function gpuMatMul(a: Float32Array, b: Float32Array, M: number, K: number, N: number) {
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter!.requestDevice();
// 创建 GPU Buffer
const bufferA = device.createBuffer({
size: (8 + M * K * 4), // 8 bytes for size + data
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
// 写入数据
const aData = new ArrayBuffer(8 + M * K * 4);
new Uint32Array(aData, 0, 2).set([M, K]);
new Float32Array(aData, 8).set(a);
device.queue.writeBuffer(bufferA, 0, aData);
// ... 类似创建 bufferB, bufferC
// 加载着色器
const shaderModule = device.createShaderModule({
code: matrixMulWGSL, // 上面的 WGSL 代码
});
// 创建计算管线
const pipeline = device.createComputePipeline({
layout: 'auto',
compute: { module: shaderModule, entryPoint: 'main' },
});
// 绑定资源
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: bufferA } },
{ binding: 1, resource: { buffer: bufferB } },
{ binding: 2, resource: { buffer: bufferC } },
],
});
// 执行计算
const commandEncoder = device.createCommandEncoder();
const pass = commandEncoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(
Math.ceil(M / 16),
Math.ceil(N / 16)
);
pass.end();
// 读回结果
const readBuffer = device.createBuffer({
size: 8 + M * N * 4,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
commandEncoder.copyBufferToBuffer(bufferC, 0, readBuffer, 0, 8 + M * N * 4);
device.queue.submit([commandEncoder.finish()]);
await readBuffer.mapAsync(GPUMapMode.READ);
const result = new Float32Array(readBuffer.getMappedRange(8));
return result.slice(); // 拷贝出来,因为 unmap 后数据就没了
}

这段代码看起来很长,但核心流程只有 4 步:创建 Buffer → 绑定到管线 → 分发计算 → 读回结果。理解了这个,你就理解了所有 WebGPU 计算的底层逻辑。

七、实际应用场景与架构建议#

WebGPU 浏览器端 AI 不是万能的,以下是我认为适合不适合的场景:

✅ 适合的场景#

  1. 隐私敏感型应用:医疗影像初筛、个人日记分析、本地密码强度检测——数据不出浏览器
  2. 离线优先应用:PWA + 本地模型,断网也能用的翻译、OCR、语音转文字
  3. 实时交互型:实时视频滤镜、手势识别、AR 试穿——延迟要求低于 50ms
  4. 降低后端成本:把推理算力分摊到用户设备,GPU 服务器成本直接砍掉
  5. 边缘场景补充:后端 API 超时/降级时,本地模型兜底

❌ 不适合的场景#

  1. 大模型(>3B):浏览器 GPU 内存有限,7B 模型勉强能跑,体验不好
  2. 训练/微调:WebGPU 目前不支持高效的反向传播
  3. 批量处理:需要处理 10000 张图片?还是用后端
  4. 低端设备覆盖:老手机、旧电脑跑不动,必须有 fallback

推荐架构:混合推理#

┌─────────────────────────────────────────────┐
│ 前端应用 │
│ │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ 本地推理引擎 │ │ 远程 API 客户端 │ │
│ │ (WebGPU) │ │ (fetch/WebSocket)│ │
│ └──────┬──────┘ └────────┬─────────┘ │
│ │ │ │
│ └────────┬───────────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ 推理路由器 │ │
│ │ - GPU 可用? │ │
│ │ - 模型已缓存? │ │
│ │ - 任务复杂度? │ │
│ │ - 网络状态? │ │
│ └───────────────┘ │
└─────────────────────────────────────────────┘
inference-router.ts
class InferenceRouter {
private localEngine: LocalInference | null = null;
private remoteClient: RemoteAPI;
async infer(input: string, options: InferOptions): Promise<InferResult> {
// 决策逻辑
const useLocal = await this.shouldUseLocal(options);
if (useLocal) {
try {
return await this.localEngine!.run(input, options);
} catch (e) {
console.warn('本地推理失败,回退到远程:', e);
return await this.remoteClient.infer(input, options);
}
}
return await this.remoteClient.infer(input, options);
}
private async shouldUseLocal(options: InferOptions): Promise<boolean> {
// 1. 本地引擎是否就绪
if (!this.localEngine?.isReady()) return false;
// 2. 任务复杂度是否在本地能力范围内
if (options.maxTokens > 2000) return false;
// 3. 是否需要隐私保护
if (options.privacy === 'strict') return true;
// 4. 网络状态
if (!navigator.onLine) return true;
// 5. 默认:小任务本地,大任务远程
return options.expectedComplexity === 'low';
}
}

八、2026 年的 WebGPU AI 生态现状#

最后聊聊生态。经过两年多的发展,WebGPU AI 已经不是实验品了:

成熟的推理框架:

  • Transformers.js v3:Hugging Face 官方,支持 100+ 模型架构,WebGPU 后端稳定
  • ONNX Runtime Web:微软出品,ONNX 格式通吃,WebGPU EP 性能优秀
  • web-llm:专注 LLM,支持 Llama/Qwen/Phi 系列,量化方案成熟
  • MediaPipe:Google 的视觉 AI 套件,人脸/手势/姿态识别开箱即用

浏览器支持:

  • Chrome 113+ ✅ (2023.05 起)
  • Edge 113+ ✅
  • Firefox 130+ ✅ (2024 下半年起)
  • Safari 18+ ✅ (iOS 18 / macOS Sequoia)

当前局限:

  • GPU 内存上限受浏览器限制(通常 1-4GB)
  • Shader 编译首次较慢(后续会缓存)
  • 移动端性能仍然不如桌面端
  • 调试工具还不够成熟(Chrome DevTools 正在完善)

总结#

WebGPU 不只是”WebGL 的下一代”,它是浏览器获得通用 GPU 计算能力的标志。对于前端工程师来说,这意味着:

  1. 你可以在前端做以前只有后端能做的事——AI 推理、科学计算、实时仿真
  2. 隐私和离线能力成为前端应用的竞争优势——数据不出设备
  3. 混合推理架构会成为标配——不是替代后端,而是互补

如果你今天只能做一件事,我建议:打开 chrome://gpu,确认你的 WebGPU 可用,然后用 Transformers.js 跑一个模型试试。 当你第一次看到浏览器里的 AI 模型实时输出结果时,你会和我一样兴奋的。

前端的边界,又往前推了一步。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

WebGPU 实战:在浏览器里跑 AI 模型,前端的下一个超能力
https://boke.hackerdream.xyz/posts/webgpu-browser-ai-inference/
作者
晴天
发布于
2026-04-15
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
晴天
Hello, I'm 晴天.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
125
分类
17
标签
287
总字数
257,955
运行时长
0
最后活动
0 天前

目录