端侧模型上线翻车实录:性能优化做完之后,用户还是不满意
摘要: 端侧模型推理优化到2秒后上线,却接连遭遇UI卡死、白屏、用户预期错位三个"翻车"现场。本文复盘了流式输出阻塞主线程、模型加载无保护、用户误以为端侧AI是ChatGPT三个真实案例,并给出了requestAnimationFrame节流、加载进度展示、输入意图判断等具体修复方案。文末附端侧模型上线检查清单,适合正在做浏览器端AI部署的开发者参考。
先说一句
前面写了端侧模型推理性能优化,把推理速度从 5 秒左右干到了 2 秒上下。当时觉得——快了,稳了,可以上线了。
结果上线第二天,用户反馈就来了。
不是"这个 AI 不够聪明",也不是"跑得太慢"。而是更直接的问题——“页面卡死了”“点不动了”“这个功能是不是坏了”。
性能优化搞定了,但用户体验还是翻车了。而且翻车的姿势,跟我想的完全不一样。
翻车一:推理快了,UI 卡了
第一个反馈来自一个用户,他用的是一台老款安卓中端机。
他说:“点了一下 AI 分析,页面就动不了了,等了十几秒才好。”
我当时的第一反应是——推理速度不是已经优化到 2 秒了吗?怎么还会卡?
排查了一圈,发现问题的根因是:流式输出的 token 回传频率太高,主线程来不及渲染。
之前为了做流式输出,我让推理线程每生成一个 token 就通过 postMessage 发给主线程。在高端机上,这个频率没问题——主线程处理得过来。但在低端机上,主线程的渲染能力跟不上,消息队列越积越多,UI 就卡死了。postMessage 的通信机制在 MDN 文档 里有详细说明,但文档不会告诉你的是——高频消息在低端机上会压垮主线程的事件循环。
我写了个简单的模拟,大概能看出问题:
// 为啥要看这段:流式输出频率太高,低端机 UI 扛不住
// ❌ 问题代码:每生成一个 token 就发消息
// 推理线程
function onToken(token) {
postMessage({ type: 'token', data: token }); // 高频发送
}
// 主线程
worker.onmessage = (e) => {
if (e.data.type === 'token') {
appendToUI(e.data.data); // 每次都要渲染,低端机扛不住
}
};
跑起来之后,在低端机上的表现是这样的:
[低端机 - 老款安卓]
推理线程:每秒发送十几个 token
主线程渲染:每秒最多处理不到十次
消息队列:持续堆积,几秒后 UI 线程阻塞
→ 用户看到页面卡死
解决方式倒不复杂——加一个节流,不要每来一个 token 就渲染,而是攒一批再渲染:
// 为啥要看这段:节流控制渲染频率,低端机也能流畅显示
let buffer = [];
let timer = null;
worker.onmessage = (e) => {
if (e.data.type === 'token') {
buffer.push(e.data.data);
// 用 requestAnimationFrame 节流,跟浏览器刷新率对齐
// MDN 上有 requestAnimationFrame 的详细说明:https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame
if (!timer) {
timer = requestAnimationFrame(() => {
appendToUI(buffer.join(''));
buffer = [];
timer = null;
});
}
}
};
节流之后,低端机上的 UI 卡死问题解决了。用户看到的是"文字一批一批地出现",而不是"每出现一个字页面就抖一下"。
这个翻车的根因:做性能优化的时候,我只盯着推理速度,没想过输出端的渲染能力也是瓶颈。端到端的性能优化,不能只看模型推理这一段。
翻车二:模型还没加载完,用户就点了
第二个反馈来自一个性急的用户。
他打开页面,看到"AI 分析"按钮,直接点了。然后页面白屏了十几秒,他以为功能坏了,关掉了页面。
这个问题的根因其实更简单:模型加载需要时间,但没有做好加载状态的保护。
我之前觉得——离线包预加载已经优化过了,模型加载从大半分钟降到了几秒左右,应该没问题了吧?(关于离线包预加载的具体方案,可以参考之前那篇端侧模型内存调优的文章。)
但用户不知道这几秒的存在。他看到按钮,就想点。
// 为啥要看这段:没做好加载保护,用户点了就是白屏
// ❌ 问题代码:按钮一直可用,模型没加载完就触发推理
button.addEventListener('click', async () => {
// 如果模型还没加载完,这里会抛异常或者卡住
const result = await model.generate(prompt);
display(result);
});
修复方案也简单——模型加载完成前,按钮置灰,并显示加载进度。Transformers.js 的 pipeline 函数支持 progress_callback 参数:
// ✅ 修复:模型加载完成前,按钮不可用
button.disabled = true;
button.textContent = '模型加载中...';
// 通过 progress_callback 监听加载进度
const generator = await pipeline('text-generation', 'Xenova/Qwen2.5-1.5B-INT4', {
progress_callback: (progress) => {
button.textContent = `加载中 ${Math.round(progress * 100)}%`;
}
});
// 加载完成后启用
button.disabled = false;
button.textContent = 'AI 分析';
这个修复上线后的效果:
[用户操作流程]
用户打开页面 → 看到按钮灰色 "加载中 0%"
几秒后 → "加载中 100%" → 按钮变为 "AI 分析"
用户点击 → 模型已就绪,直接推理 → 无白屏
这个翻车的根因:我默认了"用户会等模型加载完",但用户的行为不会按你的预期来。加载状态保护不是体验优化,是基本功能。
翻车三:用户以为这个 AI 是 ChatGPT
第三个反馈最扎心。
一个用户用完之后说:“这个 AI 不太行,我问它’帮我写一份产品需求文档’,它回了一堆废话。”
我看了下日志,端侧模型确实回了一堆废话——1.5B 的小模型,你让它写 PRD,它哪会啊。
这个问题的根因是:用户的预期错了,但 UI 上没有纠正这个预期。
用户看到"AI 功能"四个字,默认以为这是 ChatGPT 级别的能力。而端侧模型的能力上限(1.5B 参数,INT4 量化,浏览器环境)跟 ChatGPT 差了不止一个数量级。
预期和现实的差距,直接转化为差评。
我的做法是在 UI 上加了一行小字:
端侧 AI 模型,适合简单问答和摘要。复杂任务建议使用云端版本。
同时,在用户输入时做了一个简单的判断——如果输入长度超过一定字数或者包含"写"“创作”"分析"等关键词,弹一个提示:“这个问题比较复杂,切换到云端模型效果更好”。
// 为啥要看这段:在用户输入时判断是否适合端侧处理
function shouldUseCloud(prompt) {
// 简单规则:长文本或含复杂任务关键词,推荐走云端
const complexKeywords = ['写', '创作', '分析', '设计', '方案', '报告'];
const hasComplexTask = complexKeywords.some(k => prompt.includes(k));
const isLongText = prompt.length > 50;
return hasComplexTask || isLongText;
}
实际跑起来的效果,看看用户的输入命中情况:
[线上数据 - 1000 次请求抽样]
输入命中"复杂关键词":342 次(34.2%)
输入长度 > 50 字:218 次(21.8%)
命中任一条件:412 次(41.2%)
→ 这部分请求被引导到云端,端侧只处理简单任务
这套逻辑跑起来之后,端侧模型处理的任务量下降了差不多四成,但用户满意度反而上去了。
这个翻车的根因:技术能力不等于用户体验。用户不关心你用的是端侧还是云端,他只关心"这个 AI 能不能帮我解决问题"。 如果你的能力边界跟用户预期有差距,你得主动管理这个预期,而不是让用户在用了之后自己发现。
端侧模型上线检查清单
三个翻车踩下来,我整理了一份上线前检查清单,每次发版前过一遍:
| 检查项 | 翻车案例 | 检查方式 | 严重程度 |
|---|---|---|---|
| UI 线程是否会被推理输出阻塞 | 翻车一 | 在低端机上实测流式输出,观察 UI 响应 | 🔴 必须过 |
| 模型加载状态是否有保护 | 翻车二 | 模型加载过程中点击按钮,观察是否白屏/报错 | 🔴 必须过 |
| 用户预期是否已管理 | 翻车三 | 新人是否知道"这是端侧模型,能力有限" | 🔴 必须过 |
| 低端机是否有降级方案 | 翻车一 | 在 3 年前的设备上跑完整流程 | 🟡 建议过 |
| 网络异常时是否有兜底提示 | — | 断网环境下测试,看是否优雅降级 | 🟡 建议过 |
| 冷启动到首次可用的时间是否 ≤ 3 秒 | 翻车二 | 从页面加载到模型就绪,计时 | 🟡 建议过 |
边界说明
上面这些翻车和修复方案,有一个前提:模型本身是能用的。 如果你的模型选型本身就有问题(比如用 500M 的模型做复杂推理),那再多 UI 优化也救不了。
另外,这套检查清单主要针对浏览器端侧推理场景。如果是原生 App 里跑端侧模型,UI 线程的卡顿问题可能没那么严重(原生渲染性能更好),但加载保护和预期管理的问题是一样的。
回头看
三个翻车,根因其实都指向同一个问题:我太关注"模型能不能跑",而忽略了"用户怎么用"。
内存优化、性能优化,解决的是"模型能不能跑、跑得快不快"的问题。但用户不关心你用的是 INT4 还是 INT8,不关心 WebGPU 有没有走对。他只关心——“我点了,它动了没?它给的东西能不能用?”
说实话,这些问题在写代码的时候很容易忽略。因为你在开发环境用的是高端机,模型已经加载好了,网络也稳定。但用户的环境千差万别,他们的体验才是真实的。
两篇下来,端侧模型从"内存降到 500MB",到"推理干到 2 秒",再到"上线不被用户骂"——算是走完了一个完整的产品闭环。如果你也在搞端侧 AI,建议别只看技术指标,用户体验的那根弦也得绷着。
更多推荐


所有评论(0)