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

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

相关推荐
如此这般英俊20 小时前
手搓Claude Code-第十三章 background_tasks
前端·人工智能·chrome·python·算法·语言模型·自然语言处理
FlagOS智算系统软件栈20 小时前
CUDA Tile IR 接入 FlagOS 多芯片统一编译器 FlagTree,加速 AI 芯片生态迈向“开放计算”
人工智能·深度学习·flagos
Είναι η κοπέλα20 小时前
PyTorch 模型导出与部署实战:ONNX + onnxruntime(可直接落地)
人工智能·pytorch·python
qq_4542450320 小时前
本地 LLM 联调(LocalLlm / LocalLlmHttp):完全模拟调用与显式上下文传递
人工智能
ltqvibe20 小时前
Agent OS:企业智能体的控制平面
人工智能·平面·agent·智能体·企业ai
魔点科技21 小时前
一款终端, N 种场景!三端开放架构,解锁空间智能无限可能
人工智能·智能硬件·空间智能·智能终端·魔点科技
飞哥数智坊21 小时前
交付的,正在从软件变成能力
人工智能·ai编程
武子康21 小时前
DeepSeek Harness:一次 Prompt 如何变成 Turn、Step 与工具事件
人工智能·llm·agent
运维行者_21 小时前
预测性云监控怎么做?AI驱动的7大核心能力与落地路径
服务器·开发语言·网络·数据库·人工智能·python·php
赋创小助手21 小时前
机器人研发负载拆解:数据、仿真、训练与推理分别需要哪些计算资源?
服务器·人工智能·机器人·具身智能·gpu计算