摘要: 端侧模型推理优化到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,建议别只看技术指标,用户体验的那根弦也得绷着。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐