ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

浏览器本地跑大模型:WebGPU与JavaScript库的LLM推理实战

2026/9/9 1:25:00 拓冰建站 浏览量
浏览器本地跑大模型:WebGPU与JavaScript库的LLM推理实战 前阵子接了个挺有意思的需求把团队内部的文档问答助手做成一个纯静态网页用户打开浏览器就能用但所有对话数据绝不能出内网。云端 API 这条路直接堵死本地起 Python 服务又没法塞给每个不懂技术的同事。折腾了一圈我把方案落在了浏览器端本地推理上——不仅跑通了还顺手用 Three.js 把 token 生成过程做成了一段相当唬人的 3D 动画。整个过程用到的核心库就三个WebLLM、Transformers.js、Three.js。这篇文章就把这段从“异想天开”到“真的能跑”的过程完整拆开来讲顺便把坑也一并倒了。先说结论浏览器跑 LLM 这件事两年前还只是个噱头现在确实到了可以落地的阶段尤其适合数据敏感、设备可控、预算有限的内部工具场景。它不是什么万能银弹但在正确的场景里体验能超出预期。1. 浏览器跑 LLM 这事到底靠谱吗1.1 一条被低估的推理路线很多人听到“浏览器里跑大模型”第一反应是浏览器不是用来打开网页的吗模型推理不是应该发生在服务器吗其实这两年底层硬件能力已经悄悄发生了变化。过去浏览器想调用 GPU只有 WebGL 这一条路而 WebGL 是给图形渲染设计的做通用计算非常憋屈。模型稍大一点就得退回到 JavaScript 虚拟机里硬算那速度基本告别实用。但现在不一样了WebGPU 这套规范成熟起来之后浏览器终于有了面向现代 GPU 的通用计算接口可以在着色器阶段直接操作缓冲区、跑 compute shader。你可以把 GPU 想象成一个超大的分拣流水线CPU 是快递员一件件处理GPU 是几百条传送带同时运转。LLM 推理这种“大量简单计算并行执行”的任务天生就该让 GPU 来干。再加上 WebAssembly 这几年性能追赶得很快内存管理、二进制指令集都比纯 JavaScript 高效太多。于是就有了一个看起来很自然的组合用 JavaScript 做上层调度用 WebGPU 做底层计算引擎用 WASM 做算子和数据结构的承载。浏览器端推理就是从这套组合里长出来的。1.2 到底适合谁用不适合谁用我知道很多人关心的是这玩意儿能替代云端 API 吗答案是看场景。我把浏览器本地推理和传统服务端 API 推理做了个对比特点非常明显对比维度浏览器本地推理云端 API 推理数据隐私数据完全不离开设备需要把对话内容发送到服务端联网要求模型加载后可完全离线必须保持在线使用成本无 token 费用纯本地算力按 token 计费长期成本高模型规模受设备显存和内存限制可运行百亿/千亿级大模型响应速度无网络延迟依赖本地算力受网络波动影响首体验门槛需下载数百 MB 到数 GB 模型打开即用适合的场景很清晰第一是数据敏感的行业比如金融、医疗、政府、企业内部知识库对话内容不允许经第三方服务器第二是离线环境比如展厅演示、户外设备、军工涉密场所第三是成本敏感的小团队十几个人的内部问答工具完全没必要为每次对话付 API 费用第四是教育演示场景学生打开网页就能看到模型推理过程比放几张架构图直观得多。不适合的场景也有比如需要超大上下文窗口的代码仓库分析、需要调用大量外部工具的复杂 Agent、高并发对外服务的产品。这些需求在浏览器端硬扛既不稳定也不经济。我的建议很朴素能上服务端就上服务端只有在“不方便用服务端”的场景里浏览器端推理才值得作为第一选择。2. 三个 JavaScript 库的分工与选型2.1 WebLLM走 WebGPU 的推理主力这个组合里最核心的库是 WebLLM来自 MLC-LLM 项目团队。它的思路是把 LLM 的整个推理栈——权重、算子、采样器、KV cache 管理——全部编译成可以在 WebGPU 上直接运行的版本。你在浏览器里调它本质上跟调一个本地的推理服务差不多只不过这个服务跑在你的显卡里。WebLLM 最让我欣赏的一点是 API 设计几乎照搬了 OpenAI 的客户端风格。写过 OpenAI SDK 的人上手这个库几乎零学习成本。模型加载、流式输出、stop 条件、对话模板封装这些全都内置了。加载一个模型只需要指定模型标识库会自动去 Hugging Face 上拉取对应的 MLC 预编译权重。它支持 Qwen、Llama、Phi、Gemma 这些主流系列的量化版本比如 q4f32 这种常见的 4-bit 量化格式在模型体积和推理质量之间取得了不错的平衡。实际体验下来WebLLM 的性能确实对得起“浏览器端推理主力”这个定位。在 Apple Silicon 和带独显的 Windows 机器上1.5B 级别的量化模型能跑出十几到二十几 token 每秒跟 Python 端 ONNX Runtime 的差距已经缩小到可以接受的程度。这也让后续所有功能都建立在“推理速度真的能用”这个前提上。2.2 Transformers.js兜底兼容的老朋友WebLLM 很优秀但它有个硬前提浏览器必须支持 WebGPU。如果用户用的是老版本浏览器、Firefox或者一堆企业定制的安全浏览器WebLLM 直接就没辙了。所以我在架构里保留了第二套方案Transformers.js。Transformers.js 是 Hugging Face 团队出的 JavaScript 库原理是把 PyTorch 模型转成 ONNX 格式然后交给 ONNX Runtime Web 去执行。它的兼容性非常好只要浏览器能跑 WebAssembly基本就能跑 Transformers.js。虽然速度比不上 WebGPU 路线对模型的量化要求也更苛刻但它胜在“哪都能跑”和“生态庞大”。Hugging Face 上有大量已经转好格式的 ONNX 模型可以直接拉下来用不需要自己处理权重转换。我选择它的逻辑很简单WebGPU 支持情况好时用 WebLLM 追求性能遇到不支持 WebGPU 的环境就自动降级到 Transformers.js 跑一个更小的模型。两套路线互不冲突反而形成了互补。这个降级策略一开始就写进了架构后面实际使用中也救了我好几次比如在一次现场演示时演示机的浏览器恰好没开 WebGPU靠 Transformers.js 扛住了场面。2.3 Three.js让推理过程可感知第三位选手是 Three.js。说到 Three.js多数人第一反应是“做 3D 网页游戏”确实它最知名的应用场景是 WebGL 可视化。但在这个项目里Three.js 承担的是一个很多人忽视的任务让 LLM 的推理过程变得可见、可感知。用过 LLM 的人都知道模型生成是有延迟的尤其是本地推理可能要好几秒才能蹦出第一个 token。如果界面上没有任何反馈用户第一反应是“网页卡死了”然后就开始狂点刷新。这个问题靠普通 loading 转圈解决不了因为 loading 无法传递“模型正在一段一段思考”的感觉。我用 Three.js 搭建了一个 3D 场景模型加载阶段显示一个旋转的进度环推理阶段每一个新的 token 都会变成一个从场景深处飞向前方的粒子色彩和运动速度会随生成节奏变化。用户看到粒子不断涌出就能直观感觉到模型在“输出内容”焦虑感一下子就降低了。选 Three.js 还有一个现实原因它对开发者的友好度极高文档齐全、示例库庞大社区里能抄的现成代码比任何一个 WebGPU 原生方案都多。与其自己用原生 WebGL 调相机、调光照不如直接站在 Three.js 的肩膀上。3. 手把手搭一个本地 LLM 演示台3.1 初始化工程与依赖整个前端工程我用了 Vite 做脚手架没有引入复杂框架就是纯 JavaScript方便让团队里任何前端都能快速接手。npm create vitelatest browser-llm-demo -- --template vanilla cd browser-llm-demo npm install mlc-ai/web-llm huggingface/transformers three安装完成后目录结构保持默认即可。我习惯把页面入口保持在index.html把逻辑拆成三个模块llm-webllm.js、llm-wasm.js、visualizer.js分别负责 WebLLM 主线推理、Transformers.js 降级推理、Three.js 场景渲染。这样拆的好处是每个文件职责单一调试的时候不用反复滚动页面看几百行代码。需要特别提醒的是部署环境。浏览器对本地推理有一个安全上下文的要求navigator.gpu和很多能力接口都必须在 HTTPS 或者localhost环境下才可用。如果你在局域网内用 IP 访问页面浏览器默认是不给开 GPU 接口的所以内部测试阶段最好先通过反向代理加一层 HTTPS或者直接用localhost访问。3.2 用 WebLLM 加载模型并流式对话核心链路非常简单先看一段完整代码import { CreateMLCEngine } from mlc-ai/web-llm; const engine await CreateMLCEngine(Qwen2.5-1.5B-Instruct-q4f32_1-MLC, { initProgressCallback: (report) { const percent Math.round(report.progress * 100); updateProgressBar(正在加载模型${percent}%); }, }); async function sendMessage(content) { const messages [ { role: system, content: 你是一个严谨、简洁的文档问答助手。 }, { role: user, content }, ]; const chunks await engine.chat.completions.create({ messages, stream: true, temperature: 0.8, max_tokens: 1024, }); let answer ; for await (const chunk of chunks) { const delta chunk.choices[0]?.delta?.content ?? ; answer delta; renderAnswer(answer); } }为什么选 Qwen2.5-1.5B-Instruct-q4f32_1两个原因一是 Qwen 系列的中文能力在同类小模型里属于第一梯队用于内部文档问答很合适二是 q4f32 量化后的权重文件大约在 1GB 出头加载时间和内存占用都在可接受范围内。如果想追求更快的速度可以换 0.5B 版本但回答质量会明显下降想提升质量可以上 7B 版本但普通办公笔记本就很难流畅跑了。这里面有个细节必须说明WebLLM 加载模型的过程是异步的期间 GPU 会有大量编译和权重搬运所以首次加载会有明显卡顿页面一定要有进度反馈。另外模型标识里的Instruct后缀不是随意的它对应着特定的对话模板处理如果不带 Instruct 后缀模型会不知道什么时候该停止说话容易出现胡言乱语。3.3 加一层 Transformers.js 降级策略有了 WebLLM 主线之后降级方案就好写了。核心就是判断当前浏览器是否支持 WebGPUfunction isWebGPUSupported() { return typeof navigator ! undefined gpu in navigator; }如果支持就走 WebLLM 逻辑如果不支持就用 Transformers.js 加载一个小号模型import { pipeline } from huggingface/transformers; async function createWasmEngine() { const pipe await pipeline(text-generation, onnx-community/Qwen2.5-0.5B-Instruct); return { async chat(messages) { const prompt messages.map(m ${m.role}: ${m.content}).join(\n); const output await pipe(prompt, { max_new_tokens: 256, temperature: 0.7, do_sample: true, }); return output[0].generated_text; }, }; }注意Transformers.js 通常是一次性返回完整结果不像 WebLLM 支持真正的流式。虽然新版本也在尝试支持流式但考虑到兼容层本来就是“保底方案”体验稍差一点可以接受。更稳妥的做法是给用户一个显性的“轻量模式”开关让用户在老旧设备上主动选择避免自动降级后模型能力不足导致误解。3.4 用 Three.js 做 token 可视化这部分是项目里最出彩的地方代码本身并不复杂核心思路是拿 Three.js 搭一个场景然后每次收到新的 token 增量时往场景里发射一个粒子。import * as THREE from three; const scene new THREE.Scene(); const camera new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.z 8; const renderer new THREE.WebGLRenderer({ alpha: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.getElementById(canvas-wrap).appendChild(renderer.domElement); const particles []; function spawnParticle(token) { const geometry new THREE.SphereGeometry(0.06, 8, 8); const material new THREE.MeshBasicMaterial({ color: token.length 2 ? 0xff6633 : 0x33aaff, }); const mesh new THREE.Mesh(geometry, material); mesh.position.set((Math.random() - 0.5) * 2, -3, -2); particles.push(mesh); scene.add(mesh); } function animate() { requestAnimationFrame(animate); particles.forEach((mesh, i) { mesh.position.y 0.08; if (mesh.position.y 4) { scene.remove(mesh); particles.splice(i, 1); } }); renderer.render(scene, camera); } animate();在流式回调里面调用spawnParticle(delta)就行。我加了一点小设计普通 token 用蓝色中文 token 长度较长用橙色这样画面会形成“中文密集处泛橙”的视觉规律看起来很像模型在思考时点燃了一簇簇火花。如果你之前见过网上很火的“Three.js 3D 火箭发射动画特效”思路完全一致都是粒子系统加补间动画的玩法。这套可视化不仅能取悦用户对开发者自己监控推理状态也很有用如果粒子生成速度明显放缓基本可以判断是 GPU 资源被其他进程占用了。3.5 页面部署与首屏优化这个项目本质上还是一个纯静态页面部署很简单把dist目录丢到任何静态服务器即可。但首屏优化有几个点值得单独说。第一不要一打开页面就加载模型。模型文件动辄 1GB在网速一般的内网环境里可能要等好几分钟。我的做法是默认页面只加载 UI 和 Three.js 场景等用户点击“启动模型”按钮后才触发模型加载同时显示下载进度。第二用 Cache Storage 做模型文件缓存第二次访问时直接从本地缓存读取加载速度能快一个量级。具体实现可以监听 Service Worker 的fetch事件把带特定前缀的模型请求优先拦截。第三把模型资源放到独立的 CDN 路径下不要跟页面静态资源混在一起这样后续更新模型版本时页面代码和权重文件可以独立回滚。4. 踩坑记录与性能优化4.1 模型下载从磨蹭到秒开这个项目第一个大坑就是首次模型下载。WebLLM 默认从 Hugging Face 的公共仓库拉模型但在某些网络环境下从公共模型仓库拉取 1GB 文件的过程非常痛苦进度条几十 KB 地跳看着能把人急死。解决办法是把模型资源提前下载到自己的对象存储或者 CDN 上。WebLLM 支持通过初始化参数覆盖模型的 URL 前缀你只需要把 MLC 预编译权重目录完整地传到自己服务器上然后指定新的基础地址。传文件的时候一定要保持目录结构不变MC 区块链文件、参数文件、tokenizer 配置缺一不可少一个都会导致加载失败。另外我强烈建议在发布前就提前把模型文件拉齐而不是让用户现场等待。部署后可以写一个预热脚本用无头浏览器访问页面触发加载流程让 CDN 节点先把模型权重缓存到位。实测下来预热后的内网访问速度能控制在几秒内加载完 1GB 文件用户体验完全是两个级别。4.2 内存溢出与 WebGPU 设备丢失跑了一段时间后我发现页面会随着对话轮数增加变得越来越卡最后直接白屏报错。排查后发现问题出在两个地方一是 LLM 推理时 KV cache 会持续占用显存对话轮数多了显存就会被逐渐吃满二是某些版本的 WebGPU 实现在页面长时间运行后不会主动回收资源导致内存只增不减。对策也很直接限制单次会话的对话轮数比如最多 8 轮达到上限后自动清空上下文并重建推理引擎。同时监听 WebGPU 设备的lost事件一旦出现设备丢失就重新初始化避免页面直接白屏。const adapter await navigator.gpu.requestAdapter(); const device await adapter.requestDevice(); device.addEventListener(lost, (event) { console.warn(WebGPU device lost:, event.message); resetEngine(); });这些逻辑一开始以为用不上结果在实际使用中救了几次急。特别是开会演示的时候如果演示机只有 8GB 内存连续对话时间一长很容易踩到这个雷。现在我会在演示前主动重启一次页面把风险降到最低。4.3 那些“看起来没问题”的兼容性坑浏览器兼容性问题比想象中隐蔽得多。我整理了一张表方便大家对照排查浏览器环境WebGPU 支持推荐策略Chrome / Edge 桌面端默认支持部分旧版本需开启 Flag优先 WebLLMSafari 桌面端macOS近版本部分支持不稳定因素多WebLLM Transformers.js 双保险iOS Safari较新版本支持 WebGPU内存受限建议小模型或 WASMFirefox 桌面/安卓不支持 WebGPUTransformers.js企业定制浏览器视内核而定普遍配置落后Transformers.js除了 WebGPU 本身的兼容性还有一个坑是安全上下文。页面必须跑在 HTTPS 或localhost下否则连navigator.gpu都不存在。如果你在局域网里部署最好给访问者配自签名证书并一次性信任否则每次打开页面都会被安全策略拦一道。移动端方面更需要谨慎我实测在手机上跑 1.5B 模型发热非常明显用几分钟就掉电厉害。如果目标平台包含手机要么把模型降到 0.5B 以下要么直接禁止在低内存设备上自动加载大模型。4.4 真实性能数据参考性能是所有人最关心的问题但也是变化最多的数据。我在几台真实设备上跑过列一个大致的参考区间设备模型实测速度MacBook Pro M1 ProQwen2.5-1.5B-Instruct-q4f3215~25 token/sRTX 3060 笔记本Llama-3-8B-Instruct-q4f328~15 token/sIntel 核显轻薄本Qwen2.5-0.5B-Instruct5~10 token/siPhone 15 ProQwen2.5-1.5B-Instruct-q4f328~12 token/s同样是 1.5B 模型M 系列芯片设备上的 WebGPU 实现明显更成熟速度波动也小。Intel 核显虽然也能跑但速度上下起伏大用户能明显感觉到“一会儿快一会儿慢”。用这些数据给需求方汇报时我会特意标注这只是体感参考WebGPU 驱动一直在更新同型号设备不同系统版本跑出来的差异可能很大。5. 后续还能怎么玩5.1 纯浏览器 RAG顺着这个项目往下走我最看好的方向是纯浏览器 RAG。既然推理能留在本地embedding 也能用 Transformers.js 在浏览器里算那整个知识库链路就都不需要服务器了。用户可以本地上传文档文档在浏览器内完成切块、向量化然后用简单的余弦相似度检索最后把检索结果喂给本地 LLM。这个方案对中小企业特别有吸引力等于把云端知识库搬到了桌面上文档不外传服务零成本。代码层面的难点主要是向量检索效率但几千个片段以内的知识库纯 JavaScript 的线性扫描也够用。5.2 语音与多模态方向另一个很自然的方向是把浏览器原生的 SpeechRecognition 和 SpeechSynthesis 接进来做一个纯前端语音助手。用户对着浏览器说话语音识别成文字本地 LLM 生成回答再用语音合成念出来整个链路全程不走服务器。如果再配合 Three.js 做一个拟人化的 3D 角色那就真的像个“虚拟助手”了。这个组合我在本地已经跑通了原型延迟主要出在语音识别环节本地模型生成反倒不是瓶颈。如果后续浏览器端语音识别模型也能像 WebLLM 一样直接调 GPU体验会再上一个台阶。这套方案跑了小一个月最大的感受是“能跑”和“好用”之间差着一堆细节。模型选型、资源预热、降级策略、可视化反馈每一步都踩了不少坑。如果你也想做类似的东西建议先从 1.5B 级别的模型起步先把端到端流程跑通再考虑怎么提升质量和体验。毕竟浏览器端推理的价值不在“替代服务器”而在于给那些服务器到不了的地方留了一扇门。