AI流式渲染打字机效果抖动?节流方案踩坑实录

上线那天我盯着对话框看了半小时,越看越烦------AI 一吐字,整段文字就抖一下,像在打摆子。产品同学截图甩群里:「这是抖屏 bug 吗?」不是 bug,是我自己写的打字机效果,把性能问题写进去了。

记一下我怎么从「一秒重排 60 次」调到「肉眼丝滑」的,中间踩的坑都挺典型。

先搞清楚抖动从哪来

SSE 推过来的 token 是一小块一小块的,有时一次来俩字,有时半个字(UTF-8 多字节被切了)。我最早的写法很朴素:

ini 复制代码
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let full = '';
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  full += decoder.decode(value, { stream: true }); // stream:true 很关键
  setText(full); // 罪魁祸首
}

decoder.decode 不加 stream: true 那次,我吃了个大亏------中文经常显示成「�」,因为一个汉字的三个字节被拆到两次 chunk 里了。加上 stream: true,解码器会缓住不完整的字节,等下一块拼齐再吐。这个坑排查了我一下午。

抖动的真凶是 setText(full)。后端快的时候一秒能 push 三四十次,每次 setState 都触发 React 重渲染整段 Markdown,长回答里有代码块、列表、表格,重排成本极高。FPS 直接掉到 20 出头。

节流,但别用现成的 throttle

第一反应是 lodash throttle,但它有个问题:尾部那次更新可能被丢掉,导致最后几个字显示不全。我改成用 requestAnimationFrame 攒一帧渲染一次:

ini 复制代码
let pending = '';
let rafId: number | null = null;

function pushChunk(chunk: string) {
  pending += chunk;
  if (rafId != null) return;
  rafId = requestAnimationFrame(() => {
    setText(prev => prev + pending);
    pending = '';
    rafId = null;
  });
}

一帧最多渲染一次,天然贴着屏幕刷新率走,60Hz 屏就是 16.6ms 一次,重排次数从一秒 40 次压到 60 次封顶但每次增量极小。结束时记得把残留的 pending flush 一次,别漏字。

打字机的「匀速」是装出来的

光攒帧还不够顺,因为后端吐字本身忽快忽慢------卡 800ms 不动,然后哗一下来 50 个字。用户体感是「一顿一顿」。我加了个本地队列做缓冲,让字符以恒定速度出来:

ini 复制代码
const queue: string[] = [];
function enqueue(s: string) { queue.push(...s); }

function tick() {
  // 队列堆积多就一帧多吐几个,别让缓冲无限膨胀
  const step = Math.max(1, Math.floor(queue.length / 8));
  const out = queue.splice(0, step).join('');
  if (out) setText(prev => prev + out);
  requestAnimationFrame(tick);
}

step 那行是我调了好几轮的------固定一帧吐一个字,遇到长回答能拖到几十秒还没吐完,用户早走了;固定吐十个又太快没了打字感。按积压量动态调速,积压越多吐越快,体验最自然。

一个没解决干净的取舍

Markdown 边流边渲染,代码块在没闭合前 ``````````` 是残缺的,highlight.js 会闪一下未高亮的纯文本再变色。我试过等代码块闭合再渲染,但那样代码块会整块「啪」地出现,破坏打字感。最后选了「先纯文本流出、闭合后再上色」,接受那一下轻微闪烁------两害相权,闪一下比卡顿强。这块到现在我也没找到完美解,有更优雅的思路欢迎评论区拍我。

模型那头我没自己折腾,对话和工具编排直接挂在讯飞 这类 MaaS 上,现成的模型 API 调就完事,我把精力全砸在前端这点丝滑上。

你们做流式渲染抖动是怎么治的?评论区交个底。

相关推荐
用户018349301691 小时前
AI对话状态管理:useReducer还是XState
人工智能
先锋部队1 小时前
给AI对话加「停止生成」按钮:abort SSE实战
人工智能
新新技术迷1 小时前
移动端H5接AI对话的坑:键盘顶起与滚动到底
人工智能
aqi004 小时前
15天学会AI应用开发(七)有了大模型为什么还要引入RAG
人工智能·python·大模型·ai编程·ai应用
用户5191495848455 小时前
libcurl Headers API 释放后重利用漏洞:跨请求复用头句柄导致堆内存安全风险
人工智能·aigc
踩蚂蚁5 小时前
自定义语音唤醒词:从训练到部署的完整链路实践
人工智能
用户5191495848455 小时前
CVE-2025-1094 PostgreSQL SQL注入与WebSocket劫持远程代码执行利用工具
人工智能·aigc
IT_陈寒7 小时前
SpringBoot自动配置这个坑,我踩进去又爬出来了
前端·人工智能·后端