1. 端侧AI的账本:把推理塞进浏览器到底图什么
1.1 从一次真实的成本焦虑说起
去年下半年,我帮一个做在线文档工具的朋友看他们的账单。他们的产品里有个"智能摘要"功能,用户选中一段文字,点一下按钮,后台调大模型API返回摘要。功能上线三个月,日活涨了四倍,但服务器账单涨了将近七倍。最要命的是,这个功能的调用量有明显的波峰——每天上午十点和晚上八点,请求量是平峰时段的三到五倍,为了扛住这两个尖峰,他们不得不常驻一批GPU实例,平峰时段这些算力基本在空转。
这就是典型的"推理成本结构性问题":算力按峰值配置,按均值付费。用户每一次点击摘要,背后都是一次完整的网络往返、一次服务端排队、一次GPU推理。哪怕只是给一句话做摘要,也得走完整套流程。
后来我们聊到一个思路:能不能把一部分推理任务从服务器挪到用户的浏览器里跑?用户自己的电脑、手机,本身就是一台性能过剩的机器,浏览器里现在也能调用GPU了。如果摘要这种轻量任务能在本地完成,服务器只负责兜底和复杂任务,账单是不是能砍下来一大截?
这个思路就是现在大家常说的端侧AI在Web场景下的落地。而支撑它的技术底座,主要是三样东西:WebGPU、WASM,以及跑在它们之上的推理引擎。
1.2 浏览器里跑推理,到底跑的是什么
先把概念理清楚。所谓"网站AI功能放用户浏览器里跑",指的是把原本在服务器上执行的模型推理过程,迁移到用户的浏览器环境中执行。用户打开网页,浏览器下载模型文件(或者模型随页面一起加载),然后利用本地的CPU、GPU完成计算,结果直接在页面上呈现,整个过程不需要把用户数据发到服务器。
这里面有几个关键角色:
- WebGPU:浏览器暴露给JavaScript的GPU访问接口。它让网页代码能够直接调用显卡做并行计算,这是浏览器里跑大模型推理的性能基础。没有它,纯靠CPU跑,速度会慢到没法用。
- WASM(WebAssembly):一种可以在浏览器里高效运行的二进制指令格式。很多推理引擎(比如ONNX Runtime、llama.cpp的Web版本)都是编译成WASM来跑的,因为它比纯JavaScript快得多,又比原生插件安全可控。
- 推理引擎:负责加载模型、管理内存、执行计算的核心组件。常见的有ONNX Runtime Web、Transformers.js、WebLLM等,它们把底层WebGPU/WASM的复杂性封装起来,让上层业务代码能像调API一样调本地模型。
这三者叠在一起,构成了浏览器端推理的完整技术栈。你可以把它理解成:WASM是发动机,WebGPU是涡轮增压,推理引擎是变速箱,三者配合,才能让模型在浏览器这个"小车"里跑起来。
1.3 省钱这件事,得算三笔账
回到标题的问题:真能省钱吗?我的结论是——能省,但不是无脑省,得看场景。要判断值不值得做,至少得算三笔账。
第一笔是服务器成本账。把推理挪到端侧,最直接的好处是服务器不再承担这部分计算。假设一个功能每天有10万次调用,每次调用在服务端消耗0.01元的GPU成本,一天就是1000元,一个月3万。如果其中70%能挪到端侧,一个月省2万多。这是最直观的收益。
第二笔是带宽与延迟账。端侧推理不需要把用户数据上传,也不需要把推理结果下载(结果直接在本地生成),省了往返带宽。更重要的是延迟——本地推理没有网络往返,没有服务端排队,响应速度往往比调API快。用户体感上就是"点了就有反应"。
第三笔是模型分发成本账。这是很多人忽略的隐性成本。模型文件本身是要下载到用户浏览器的,一个量化后的小模型可能几十MB到几百MB。如果用户每次访问都要重新下载,带宽成本会非常吓人。所以必须配合浏览器缓存策略,让模型只下载一次,后续复用。这笔账算不好,省下的服务器钱可能全搭进CDN带宽里。
注意:端侧推理不是"零成本",而是"成本转移"。服务器省下的钱,一部分变成了用户的设备算力消耗和首次加载的等待时间。是否划算,取决于你的用户愿不愿意用自己设备的电和流量,换更快的响应和更好的隐私。
1.4 哪些场景适合,哪些纯属折腾
不是所有AI功能都适合往浏览器里塞。我踩过的坑告诉我,判断标准其实很朴素:模型够小、调用够频繁、隐私要求够高、对延迟够敏感,四个条件满足得越多,越值得做。
适合的场景:
- 文本摘要、关键词提取、情感分类:模型小(几十MB以内),调用频繁,端侧跑完全够用。
- 图片背景移除、简单图像增强:借助WebGPU,图像类小模型在浏览器里跑得很流畅。
- 敏感数据的本地处理:比如用户不想上传的文档、照片,在本地处理完只输出结果,隐私性拉满。
- 离线可用功能:用户断网时依然能用的AI能力,端侧是唯一选择。
不适合的场景:
- 大语言模型的复杂对话:7B以上的模型,哪怕量化后,浏览器加载和推理都吃力,用户体验会很差。
- 需要强一致性的任务:不同用户设备性能差异巨大,端侧推理结果可能有细微差别,对一致性要求高的场景要谨慎。
- 一次性、低频的调用:用户一年用一次的功能,让他下载几百MB模型,纯属折磨。
2. 技术选型:WebGPU、WASM和推理引擎怎么搭
2.1 WebGPU不是万能药,先看兼容性
WebGPU是这套方案里性能最强的部分,但它的兼容性是绕不开的坎。截至我写这篇内容的时候,Chrome和Edge对WebGPU的支持已经比较成熟,Safari在较新版本上也跟进了,Firefox的支持相对滞后一些。移动端的支持情况更复杂,部分安卓机型虽然浏览器版本够新,但底层GPU驱动可能有问题。
所以实际项目里,我的做法是做能力检测和降级:
// 检测WebGPU是否可用 async function checkWebGPUSupport() { if (!navigator.gpu) { return { supported: false, reason: 'navigator.gpu不存在' }; } try { const adapter = await navigator.gpu.requestAdapter(); if (!adapter) { return { supported: false, reason: '无法获取GPU适配器' }; } return { supported: true, adapter }; } catch (e) { return { supported: false, reason: e.message }; } }检测到不支持WebGPU时,降级到WASM纯CPU推理。虽然慢,但至少功能可用。这个降级策略很重要,因为你的用户里一定有一部分设备跑不了WebGPU,不能让他们直接看到"功能不可用"。
实操心得:不要用UA字符串判断浏览器是否支持WebGPU,UA可以伪造,而且版本号规则混乱。永远用能力检测——直接尝试获取adapter,拿不到就降级。这是最可靠的方式。
2.2 WASM的角色:不是备胎,是基石
很多人以为WASM只是WebGPU不可用时的备胎,其实不是。WASM在这套架构里承担着更基础的角色。
推理引擎的核心计算逻辑(矩阵乘法、卷积、激活函数等)通常是用C++或Rust写的,编译成WASM后在浏览器里执行。WebGPU的作用是加速其中的并行计算部分,但引擎的调度、内存管理、算子编排这些逻辑,还是跑在WASM里。所以准确地说,WASM是主干,WebGPU是加速器。
WASM的性能相比纯JavaScript有数量级的提升,尤其是涉及大量数值计算时。但它也有局限:WASM的内存模型和JavaScript是隔离的,数据在两边传递需要拷贝,这个拷贝开销在频繁小数据交互时会成为瓶颈。好的推理引擎会尽量减少这种拷贝,比如用共享内存(SharedArrayBuffer)来传递数据。
2.3 推理引擎选型对比
市面上能用的浏览器端推理引擎有好几个,我实际用过的主要是下面这几个,做个对比:
| 引擎 | 底层依赖 | 模型格式 | 适合场景 | 上手难度 |
|---|---|---|---|---|
| ONNX Runtime Web | WASM + WebGPU | ONNX | 通用,图像/文本分类 | 中等 |
| Transformers.js | WASM + WebGPU | HuggingFace模型 | NLP任务,开箱即用 | 低 |
| WebLLM | WebGPU | MLC格式 | 大语言模型对话 | 中等 |
| llama.cpp Web版 | WASM | GGUF | 本地LLM推理 | 较高 |
选型的核心考量是模型生态和性能需求。如果你用的是HuggingFace上的现成模型,Transformers.js最省事,几行代码就能跑起来。如果要做图像类任务,ONNX Runtime Web的算子支持更全。如果要跑对话模型,WebLLM或llama.cpp的Web版更合适。
我个人的经验是:先用Transformers.js快速验证可行性,确认端侧推理效果达标后,再根据性能瓶颈决定是否换更底层的方案。不要一上来就啃最难的,容易劝退。
2.4 模型量化的取舍:精度换速度
浏览器里跑模型,绕不开量化。所谓量化,就是把模型参数从32位浮点数压缩成8位甚至4位整数,模型体积能缩小到原来的四分之一到八分之一,推理速度也大幅提升。代价是精度会有损失。
常见的量化方案:
- FP32:原始精度,体积最大,浏览器里基本不用。
- FP16:半精度,体积减半,精度损失很小,WebGPU对FP16支持较好。
- INT8:8位整数,体积缩到四分之一,精度损失可接受,适合大多数分类、摘要任务。
- INT4:4位整数,体积最小,精度损失明显,适合对精度不敏感的场景。
我的建议是:从INT8开始试。大多数任务INT8的精度损失在可接受范围内,体积和速度的收益很明显。如果INT8效果不达标,再考虑FP16。INT4除非万不得已,不建议在浏览器端用,精度掉得太厉害,用户体验会崩。
注意:量化不是简单的"砍精度",不同量化算法效果差异很大。有的量化方案会针对特定层做保护,有的会做校准。选模型时优先选社区已经量化好、口碑好的版本,自己量化容易踩坑。
3. 实操:从零搭一个浏览器端推理功能
3.1 环境准备与依赖安装
假设我们要做一个"文本情感分析"功能,用户输入一段文字,浏览器本地判断是正面还是负面情绪。这个任务模型小、调用频繁,非常适合端侧。
先建项目,装依赖。我用的是Vite做构建工具,因为它对WASM和Web Worker的支持比较友好:
npm create vite@latest browser-ai-demo -- --template vanilla cd browser-ai-demo npm install @xenova/transformers@xenova/transformers就是Transformers.js的npm包,它封装了模型加载、推理执行的全过程,底层自动处理WASM和WebGPU的选择。
3.2 模型加载与缓存策略
模型加载是端侧推理最关键的环节,直接决定用户体验。第一次访问要下载模型,后续访问必须走缓存,否则每次刷新都重新下载,用户会疯。
Transformers.js默认会用浏览器的Cache API做缓存,但需要配置一下:
import { pipeline, env } from '@xenova/transformers'; // 配置模型缓存 env.useBrowserCache = true; env.allowLocalModels = false; // 加载情感分析pipeline let classifier = null; async function initClassifier() { if (classifier) return classifier; classifier = await pipeline( 'sentiment-analysis', 'Xenova/distilbert-base-uncased-finetuned-sst-2-english', { progress_callback: (progress) => { // 显示下载进度 console.log(`加载进度: ${(progress.progress * 100).toFixed(1)}%`); } } ); return classifier; }这段代码里,env.useBrowserCache = true让模型文件下载后存入浏览器缓存,下次直接从缓存读取。progress_callback用来给用户展示加载进度,避免用户以为页面卡死。
模型选的是DistilBERT的情感分析版本,量化后大概几十MB,首次加载需要几秒到十几秒(取决于网速),后续加载基本秒开。
实操心得:模型加载一定要放在Web Worker里做,不要阻塞主线程。否则加载期间页面会卡死,用户点击没反应,体验极差。Web Worker里加载完模型后,通过postMessage通知主线程,主线程再更新UI。
3.3 推理执行与结果处理
模型加载好之后,推理就很简单了:
async function analyzeSentiment(text) { const clf = await initClassifier(); const result = await clf(text); return result; } // 使用 const result = await analyzeSentiment('这个产品太好用了,强烈推荐!'); console.log(result); // 输出: [{ label: 'POSITIVE', score: 0.9998 }]看起来简单,但实际项目里要考虑的东西不少。比如输入文本的长度限制——模型有最大token数限制,超长文本要截断或分段。再比如批量推理——如果用户一次提交多条文本,要合并成batch一起推理,比逐条推理效率高得多。
3.4 性能实测数据
我在自己的笔记本(M1芯片,16G内存)上做了几组实测,数据如下:
| 任务类型 | 模型 | 后端 | 首次加载 | 单次推理 |
|---|---|---|---|---|
| 情感分析 | DistilBERT INT8 | WASM | 约8秒 | 约45ms |
| 情感分析 | DistilBERT INT8 | WebGPU | 约6秒 | 约18ms |
| 文本摘要 | T5-small INT8 | WASM | 约12秒 | 约320ms |
| 图像分类 | MobileNet INT8 | WebGPU | 约5秒 | 约25ms |
从数据能看出几个规律:WebGPU相比WASM有2到3倍的推理速度提升,但首次加载时间差别不大(因为加载主要是网络和解析开销)。文本生成类任务(摘要)比分类任务慢一个数量级,因为要逐token生成。
这些数据说明:分类、匹配类任务非常适合端侧,生成类任务要谨慎。摘要这种任务,320ms的延迟用户勉强能接受,但如果模型再大一点,延迟上到秒级,体验就崩了。
4. 踩坑实录:那些文档里不会写的问题
4.1 内存泄漏:浏览器标签页的隐形杀手
端侧推理最容易被忽视的问题是内存。模型加载后常驻内存,推理过程中还会产生中间张量。如果代码里没有及时释放,内存会持续增长,最终导致标签页崩溃。
我遇到过一次:一个图像处理功能,用户连续处理十几张图片后,页面直接白屏。排查发现是每次推理都创建了新的张量,但没有释放旧的。WebGPU的资源管理尤其要注意,GPU buffer用完必须显式销毁。
// 错误做法:每次推理都新建,不释放 async function processImage(image) { const tensor = await createTensor(image); const result = await model.run(tensor); return result; } // 正确做法:复用张量,及时释放 let cachedTensor = null; async function processImage(image) { if (cachedTensor) { cachedTensor.dispose(); // 释放旧张量 } cachedTensor = await createTensor(image); const result = await model.run(cachedTensor); return result; }注意:不同推理引擎的释放API不一样,ONNX Runtime用
dispose(),Transformers.js的tensor也有dispose()方法。用之前查清楚,别想当然。
4.2 跨浏览器兼容的坑
不同浏览器对WebGPU和WASM的支持差异,会带来各种诡异问题。我整理了一个常见问题速查表:
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| WebGPU初始化失败 | 浏览器版本过低或GPU驱动问题 | 检查navigator.gpu,降级到WASM |
| WASM加载报错 | MIME类型配置错误 | 服务器需返回application/wasm |
| 推理结果异常 | 量化模型精度问题 | 换FP16模型对比 |
| 页面卡顿 | 推理在主线程执行 | 迁移到Web Worker |
| 内存持续增长 | 张量未释放 | 检查dispose调用 |
| 移动端崩溃 | 设备内存不足 | 限制模型大小,加内存检测 |
其中WASM的MIME类型问题特别常见。很多静态服务器默认不识别.wasm文件,返回的Content-Type不对,浏览器就拒绝加载。解决办法是在服务器配置里加上:
# Nginx配置 types { application/wasm wasm; }4.3 首次加载体验优化
模型首次加载的等待时间,是端侧推理体验的最大短板。用户打开页面,看到转圈十几秒,大概率直接关掉。我试过几种优化手段,效果比较明显的有:
预加载:在用户还没触发AI功能时,后台悄悄开始下载模型。比如用户打开页面后,延迟几秒启动模型加载,等用户真正点击功能时,模型可能已经准备好了。
分片加载:把模型文件切成多个小块,优先加载推理必需的部分,让用户能更快开始使用。这个需要推理引擎支持,不是所有引擎都能做。
进度反馈:一定要给用户看加载进度,哪怕只是个百分比。有进度条和没进度条,用户的心理等待时间差别巨大。
降级提示:如果检测到设备性能不足或网络太慢,主动提示用户"当前设备可能无法流畅运行此功能",并提供"使用云端处理"的选项。别硬撑。
4.4 省钱账的最终核算
回到最初的问题。我帮朋友那个文档工具做了端侧改造后,实际数据是这样的:
改造前,智能摘要功能每月服务器成本约3.2万元。改造后,70%的请求走端侧,服务器成本降到约1.1万元,省了2.1万。但新增了CDN带宽成本——模型文件约80MB,月活用户5万,假设每人每月平均加载1.5次(考虑缓存命中),带宽成本约增加3000元。净省约1.8万元/月。
但代价是:首次使用该功能的用户,要等8到15秒加载模型。我们通过预加载和进度提示,把用户流失率控制在了可接受范围内。如果用户流失率再高一点,省下的钱可能还不够弥补用户损失。
所以我的结论是:端侧推理能省钱,但省的是"服务器算力钱",换来的是"用户体验成本"和"工程复杂度"。这笔账划不划算,取决于你的用户规模、调用频率、以及对用户体验的容忍度。小规模、低频调用,老老实实调API更省心。大规模、高频调用,端侧推理的账才算得过来。
最后分享一个我踩过的坑:不要试图把所有AI功能都往端侧搬。我一开始贪心,想把图像识别也放端侧,结果模型太大,用户加载体验极差,最后又灰溜溜改回服务端。端侧推理是手术刀,不是锤子,找准适合它的场景,才能发挥价值。