打字机效果优化:用 requestAnimationFrame 缓冲高频文字更新

你一定知道浏览器最怕的不是"大量数据",而是**"高频小动作"**。

当 AI 通过 SSE(Server-Sent Events)流式返回数据时,由于网络包的大小不一,可能会出现一秒内触发几十次甚至上百次 DOM 更新的情况。如果你直接在接收到消息时就修改 innerText,浏览器会陷入不断的 Reflow(回流)Repaint(重绘) 循环中。这不仅会让风扇狂转,还会导致输入框卡顿、动画掉帧,甚至让你的 AI Prompt Manager 看起来像个廉价的半成品。


1. 核心矛盾:网络频率 vs. 屏幕频率

  • 网络推送:可能每 10ms 就来一个 Token。
  • 屏幕刷新:大多数显示器是 60Hz(约每 16.7ms 刷新一次)。
  • 后果 :如果在 16.7ms 内你更新了 5 次 DOM,浏览器实际上只能显示最后一次,前 4 次的计算全是浪费的 CPU 垃圾时间

2. 优化方案:构建"文字缓冲区"

我们的思路是:不要来一个字蹦一个字,而是建立一个中间缓冲区(Buffer) 。无论网络多快,我们都只跟着浏览器的 requestAnimationFrame (rAF) 节奏走。

实战代码实现

JavaScript

kotlin 复制代码
class TypewriterBuffer {
  constructor(container) {
    this.container = container;
    this.buffer = "";      // 待渲染的文字池
    this.isRendering = false;
  }

  // 1. 接收网络推送的数据
  push(chunk) {
    this.buffer += chunk;
    this.requestRender();
  }

  // 2. 触发渲染逻辑
  requestRender() {
    if (this.isRendering) return; // 已经在跑了,别催
    this.isRendering = true;

    requestAnimationFrame(() => {
      this.flush();
    });
  }

  // 3. 执行真正的 DOM 更新
  flush() {
    if (this.buffer.length > 0) {
      // 这里的逻辑可以更复杂:比如一次只从 buffer 拿 2 个字,模拟更自然的打字感
      const fragment = document.createTextNode(this.buffer);
      this.container.appendChild(fragment);
      
      this.buffer = ""; // 清空缓冲
    }
    
    this.isRendering = false;
  }
}

// 使用方式
const logger = new TypewriterBuffer(document.getElementById('ai-response'));
sse.onmessage = (e) => logger.push(e.data);

3. 进阶:模拟"真人打字"的韵律感

AI 现在的流式输出有时太快,快到人眼根本读不过来。资深开发会在这里加一点"演技":即使数据已经到了,我们也匀速释放。

JavaScript

arduino 复制代码
// 在 flush 中增加步长控制
flush() {
  const step = Math.ceil(this.buffer.length / 5); // 动态步长,量大时快点,量小时慢点
  const textToAppend = this.buffer.slice(0, step);
  this.buffer = this.buffer.slice(step);

  this.container.lastChild.textContent += textToAppend;

  if (this.buffer.length > 0) {
    requestAnimationFrame(() => this.flush()); // 递归渲染剩余字符
  } else {
    this.isRendering = false;
  }
}

4. 性能避坑指南

  1. 避免 innerHTML :在流式场景下,严禁使用 innerHTML += chunk。这会导致浏览器解析整个字符串并重新构建所有 DOM 节点。请使用 textContentappendChild(TextNode)
  2. 长文本崩溃 :如果 AI 回复了 5000 字,lastChild.textContent 的性能也会下降。建议每 1000 个字封装进一个独立的 <div><span>
  3. 合并更新 :如果你的 AI 包含 Markdown 解析(如 marked.js),不要每来一个字都调用一次解析器。应该利用 rAF 的间隙,每隔 5-10 个 Token 解析一次全量 Markdown。
  4. CPU 保护 :当页面处于后台(用户切走了)时,requestAnimationFrame 会自动暂停。这正是我们要的效果:用户看不见时,不浪费一丁点 CPU 算力。

5. 效果对比

维度 直接 DOM 更新 rAF 缓冲区更新
CPU 占用 剧烈波动,易出现峰值 平滑且稳定
渲染流畅度 视觉闪烁,可能有撕裂感 丝滑,符合屏幕刷新率
主线程压力 持续高压,响应输入慢 间歇性工作,输入依旧流畅
适用场景 简单、低频更新 AI 聊天、金融实时行情

相关推荐
神探小白牙15 分钟前
eCharts 多系列柱状图增加背景图
javascript·ecmascript·echarts
女生也可以敲代码20 分钟前
AI时代下的50道前端开发面试题:从基础到大模型应用
前端·面试
ZhengEnCi27 分钟前
M5-markconv自定义CSS样式指南 📝
前端·css·python
IT_陈寒1 小时前
SpringBoot自动配置的坑差点让我加班到天亮
前端·人工智能·后端
xingpanvip1 小时前
星盘接口开发文档:星相日历接口指南
android·开发语言·前端·css·php·lua
@PHARAOH1 小时前
WHAT - GitLens supercharged 插件
前端
TT模板1 小时前
苹果cms整合西瓜播放器XGplayer插件支持跳过片头尾
前端·html5
Wect2 小时前
React 性能优化精讲
前端·react.js·性能优化
追风筝的人er2 小时前
SpringBoot+Vue3 企业考勤如何处理法定假期?节假日方案、调休补班与工作日判断链路拆解
前端·vue.js·后端
无敌的黑星星3 小时前
Java8 CompletableFuture 实战指南
linux·前端·python