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 调就完事,我把精力全砸在前端这点丝滑上。

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

相关推荐
衡石科技5 小时前
同一句业务问题,怎样持续问对数?衡石 ChatBI 的语义准备与回归验证
人工智能·数据挖掘·回归·chatbi
新新学长搞科研5 小时前
【会议征稿 | 杭州站】第七届机器学习与计算机应用国际学术会议 (ICMLCA 2026)
人工智能·机器学习·计算机
找方案5 小时前
从用户画像到智能定价:81页方案拆解“大数据决策与支撑平台”怎么落地
大数据·人工智能·数据分析·智慧城市·用户画像
紧跟先前的步伐5 小时前
Go AI Agent 实战 05|Temperature 与生成参数:让模型输出可控
java·人工智能·golang
一只爱撸猫的程序猿5 小时前
我让 Agent 干活,自己只做审核:一个真实项目的六次"人审拦截"
人工智能
镜子AI6 小时前
镜子AI工程实践:用 Python 做 GEO 实体一致性审计,并自动生成 JSON-LD 结构化数据
人工智能·python·json
liliangcsdn6 小时前
DeepSeek DeepInfer开源项目的功能分析和探索
大数据·人工智能
燕落南枝6 小时前
CSS 学习:类型选择器与 AI 注释实战
css·人工智能·学习
今天的砖头有点烫手啊6 小时前
GPT-6 Pro 拉起“AI 小队”、ChatGPT 长出“双手”:OpenAI 把 Agent 干活的方式重写了
人工智能·agi
田里的水稻7 小时前
FA_CNN卷积神经网络七---优化器PyTorch
人工智能·pytorch·深度学习·神经网络·cnn