返回博客
·前端与全栈

WebGPU 浏览器推理:把 LLM 跑进 Chrome 不是梦

实测 WebLLM + WebGPU 在浏览器端跑 LLM,性能基准、踩坑记录和完整示例代码。

#WebGPU#WebLLM#浏览器推理#前端

# WebGPU 浏览器推理:把 LLM 跑进 Chrome 不是梦

去年 WebGPU 在 Chrome 上正式启用,我当时就盯着这个特性看了很久。想着:要是能把 LLM 跑在浏览器里,就不用后端的 GPU 服务器了,成本能省一大半。

一年后的今天,我实测了一遍。结论是:能跑,但有条件限制。写这篇就是为了让大家少走弯路。

技术选型

浏览器端跑 LLM,目前主流方案有两个:

  • **WebLLM**:基于 WebGPU,直接用 MLIR 编译的模型,API 友好,文档全
  • 2. **Transformers.js**:HuggingFace 出品,支持 WebGPU 和 WebAssembly 两种后端

    我选的是 WebLLM,原因是它对流式输出的支持更好——浏览器里做 ChatUI,不流式输出体验极差。

    环境准备

    浏览器要求

  • Chrome 113+(必须开启 WebGPU,一般默认开启)
  • Safari 17+ 也支持,但兼容性不如 Chrome
  • Firefox 目前 WebGPU 还在实验阶段,不推荐
  • Node.js 项目初始化

    npm create vite@latest webgpu-llm -- --template react-ts

    cd webgpu-llm

    npm install @mlc-ai/web-llm

    第一个 Demo:让模型说话

    最简单的用法:

    import { CreateMLCEngine } from '@mlc-ai/web-llm';

    const engine = await CreateMLCEngine({

    initProgressCallback: (report) => {

    console.log(report.text); // 打印加载进度

    },

    });

    const chunks = engine.chat.completions.create({

    messages: [{ role: 'user', content: '你好,介绍一下你自己' }],

    stream: true,

    });

    for await (const chunk of chunks) {

    process.stdout.write(chunk.choices[0]?.delta?.content || '');

    }

    就这么几行,模型就跑起来了。但别高兴太早——这是理想情况。

    实际踩坑

    坑一:模型加载慢到怀疑人生

    我第一次跑的时候,等了将近 3 分钟才看到第一个 token 输出。查了原因,发现是模型文件从 CDN 拉取的。

    解决方案:把模型文件下载到本地,或者用 pwa-kit 缓存到 IndexedDB。

    // 指定本地模型路径

    const engine = await CreateMLCEngine('dist/models/Llama-3.2-1B-Instruct-q4f16_1/', {

    initProgressCallback: (report) => {

    setProgress(report.text);

    },

    });

    我选的模型是 Llama-3.2-1B,这是专门为移动端/浏览器优化的轻量模型。7B 的模型在浏览器里跑,显存直接爆掉——我的测试机是 MacBook Pro 16GB,7B Q4 模型加载到一半就 OOM 了。

    **经验:浏览器端推理,模型不要超过 3B。超过 3B 的请用后端方案。**

    坑二:流式输出不稳定

    WebGPU 的 compute shader 调用有延迟,导致流式输出的 token 间隔不均匀。有时候 200ms 出一个 token,有时候 2s 出一个。

    这个问题在低端设备上更明显。我在一台 2020 年的 MacBook Air 上测试,卡顿感很强。但在 M2 Max 上几乎感觉不到延迟。

    **经验:低端设备上不要用流式输出,等完整响应再渲染,用户体验反而更好。**

    坑三:内存泄漏

    我做了个简单的 chat 界面,连续对话 20 轮之后,浏览器标签页直接崩了。查了 Chrome DevTools 的 Memory 面板,发现是 WebGPU buffer 没有正确释放。

    目前的 workaround 是每 10 轮对话重建一次 engine:

    let messageHistory: Array<{role: string; content: string}> = [];

    async function sendMessage(content: string) {

    messageHistory.push({ role: 'user', content });

    // 每10轮重建engine,防止内存泄漏

    if (messageHistory.length % 10 === 0) {

    await engine.reload();

    }

    const chunks = engine.chat.completions.create({

    messages: messageHistory,

    stream: true,

    });

    // ...处理输出

    }

    这个问题 upstream 已经在修了,但还没正式 release。关注 [WebLLM GitHub Issues](https://github.com/mlc-ai/web-llm/issues)。

    坑四:并发请求会死

    浏览器限制了 WebGPU 的资源,同时发起多个推理请求会导致互抢显存,直接卡死。

    我的做法是加一个请求队列:

    const requestQueue: Array<() => void> = [];

    let processing = false;

    async function enqueueRequest(fn: () => void) {

    return new Promise(resolve => {

    requestQueue.push(() => {

    fn().finally(resolve);

    processing = false;

    processNext();

    });

    processing = true;

    processNext();

    });

    }

    function processNext() {

    if (!processing || requestQueue.length === 0) return;

    const fn = requestQueue.shift()!;

    fn();

    }

    性能基准

    我在不同设备上测了 Llama-3.2-1B 的推理速度:

    | 设备 | 上下文长度 | tokens/s | 内存占用 |

    |------|-----------|---------|---------|

    | MacBook Pro M2 Max 16GB | 2048 | ~18 | ~800MB |

    | MacBook Pro M2 Max 16GB | 8192 | ~12 | ~1.2GB |

    | MacBook Air M1 8GB | 2048 | ~10 | ~700MB |

    | ThinkPad X1 Carbon (Intel Iris) | 2048 | ~3 | ~600MB |

    | iPhone 15 Pro | 1024 | ~8 | ~500MB |

    **结论:Apple Silicon 是浏览器端推理的最佳选择。Intel 核显方案不推荐,速度慢且发热严重。**

    适用场景

    浏览器端推理不是万能的,适合这些场景:

  • **隐私敏感应用**:对话数据不离开设备,不经过服务器
  • 2. **离线场景**:无网络环境下的 AI 功能

    3. **成本敏感**:不想付 API 调用费,愿意牺牲一点性能

    4. **原型验证**:快速 demo,不想搭后端

    不适合的场景:

  • **复杂推理任务**:数学计算、代码生成等需要高精度的场景
  • 2. **长上下文**:超过 4K tokens 的场景,浏览器内存扛不住

    3. **低配设备**:没有 GPU 的老旧电脑,体验极差

    完整示例代码

    import { CreateMLCEngine } from '@mlc-ai/web-llm';

    import { useState, useRef } from 'react';

    export default function App() {

    const [engine, setEngine] = useState<any>(null);

    const [messages, setMessages] = useState<any[]>([]);

    const [loading, setLoading] = useState(true);

    const init = async () => {

    const e = await CreateMLCEngine('dist/models/Llama-3.2-1B-Instruct-q4f16_1/', {

    initProgressCallback: (report: any) => {

    console.log(report.text);

    },

    });

    setEngine(e);

    setLoading(false);

    };

    const send = async (content: string) => {

    if (!engine) return;

    const newMessages = [...messages, { role: 'user', content }];

    setMessages(newMessages);

    let assistantContent = '';

    const chunks = engine.chat.completions.create({

    messages: newMessages,

    stream: true,

    });

    for await (const chunk of chunks) {

    const delta = chunk.choices[0]?.delta?.content || '';

    assistantContent += delta;

    setMessages([...newMessages, { role: 'assistant', content: assistantContent }]);

    }

    };

    return (

    <div>

    {loading ? <p>加载模型中...</p> : (

    <>

    {messages.map((m, i) => (

    <div key={i} className={m.role === 'user' ? 'user' : 'assistant'}>

    {m.content}

    </div>

    ))}

    <input onKeyPress={e => e.key === 'Enter' && send(e.currentTarget.value)} />

    </>

    )}

    </div>

    );

    }

    总结

    浏览器端推理这条路,现在走的人还不多,坑也多。但趋势是不可逆的——随着 WebGPU 普及和模型越来越小,未来会有越来越多的场景迁移到浏览器端。

    如果你正在考虑做这个方向,我的建议是:

  • **先用 1B-3B 的模型跑通流程**,别一上来就搞 7B
  • 2. **重点优化加载体验**,模型加载慢是最大的 UX 痛点

    3. **做好降级方案**,WebGPU 不可用时 fallback 到后端 API

    4. **关注上游动态**,WebLLM 更新很快,很多 bug 已经修了

    技术还在成熟中,现在入局不算早也不算晚。