跳到内容
当前位置:首页>面试真题>#每天一道AI面试题 |《字节前端面试真题:你是怎么做 SSE 渲染性能优化的?》

#每天一道AI面试题 |《字节前端面试真题:你是怎么做 SSE 渲染性能优化的?》

  • 2026-09-24 18:31:48
#每天一道AI面试题 |《字节前端面试真题:你是怎么做 SSE 渲染性能优化的?》

这题不是问你会不会优化,是问你有没有真被流式渲染折磨过

开头导语

这题一出来,很多人会本能地往“性能优化八股”上靠。

比如一张嘴就是:

  • • memo
  • • useMemo
  • • useCallback
  • • 节流防抖
  • • 虚拟列表

这些词当然不算错。但如果你的回答只有这些,面试官通常听不出你到底有没有做过 SSE 流式场景。

因为 SSE 渲染性能优化 这题,真正难的地方从来不是“你知不知道优化手段”。而是:你有没有在一个回答不断变长、页面不断更新、用户还在不断操作的场景里,真的把渲染链路压顺过。

这题如果答得浅,就会像在背前端通用优化词。答得深一点,应该讲的是:

  • • 你优化的到底是什么问题
  • • 这些问题为什么会在 SSE 场景里被放大
  • • 你具体做了哪些拆分、缓冲、隔离和降级
  • • 最后页面到底是怎么从“持续抖”变成“能稳定读”的

所以这题更像是在问:你做过没有,不只是你懂不懂。


正文

一、先说结论:SSE 优化的核心,不是“让它别更新”,而是“别让每次流式片段都触发整条渲染链路”

很多人一听 SSE 渲染优化,脑子里会冒出一句话:减少渲染次数。

这句话当然对。但还不够具体。

因为真正的问题不是“有渲染”。而是:每来一点新内容,你是不是就让整条消息、整块 markdown、整段代码高亮、整个滚动容器,甚至整页状态一起跟着忙。

如果是这样,那 SSE 场景就会特别容易变卡。

所以这题更准确的说法应该是:优化目标不是阻止流式更新,而是把流式更新的影响范围压到最小。

也就是说:

  • • 更新可以持续发生
  • • 但不要每次都惊动整棵树
  • • 不要每次都重跑最贵的解析链路
  • • 不要每次都打断用户当前的阅读和滚动

这才是 SSE 渲染优化真正的味道。


二、第一步通常不是 memo,而是先控制更新频率

这是最先该做的。

很多 SSE 场景一开始卡,不是因为单次 append 特别重。而是因为后端吐流太频繁,你前端来一片就 setState 一次。

这样页面就会进入一种持续繁忙状态:

  • • 当前消息不断增长
  • • 组件不断重渲染
  • • diff 不断执行
  • • 滚动区域不断变化
  • • 用户刚想读一眼,下一波更新又到了

所以我一般会先做一层很现实的优化:不要让 UI 更新频率直接等于服务端吐流频率。

常见做法就是:

  • • 先把片段缓存在 buffer 里
  • • 按时间片合并更新
  • • 或者按一定字符量、片段量批量刷到界面

这样做的好处很直接:服务端可以继续细粒度吐流。但前端不需要用同样细的频率去重渲染。

这一步往往就能解决一大半“页面一直细碎抖动”的问题。


三、第二步:把“正在生成的消息”和“历史消息”隔离开

这一步非常关键。

很多对话页面一开始写得比较直,流式消息一更新,就把整个 messageList 整体 set 一次。逻辑看着没问题。性能 usually 就开始有问题了。

因为历史消息本来已经稳定了。它不该跟着当前流一起反复 render。

所以一个很重要的优化思路就是:把“当前正在生成的那条消息”和“已经完成的历史消息”拆开。

比如可以理解成:

  • • 历史区域尽量冻结
  • • 当前流式区域单独活跃

这样每次追加内容,影响范围就不会扩到整个历史列表。

否则你会得到一种很典型的浪费:当前只是多了几个字,结果上面十几轮历史消息组件也陪着一起跑了一遍。

这就有点冤。


四、第三步:富文本链路不要在最频繁更新的时候全量执行

这也是 SSE 场景里特别常见的坑。

很多 AI 对话产品会把回答渲染成:

  • • markdown
  • • 标题
  • • 列表
  • • 表格
  • • 代码块
  • • 高亮
  • • 引用

看起来很舒服。但如果你每收到一小段流,就把整段全文重新做一遍:

  • • markdown parse
  • • AST 构建
  • • code highlight
  • • DOM 重建

那性能基本很难好看。

所以这里的核心不是“要不要富文本”。而是:富文本链路要不要分阶段执行。

更靠谱的做法通常是:

  • • 流式阶段先尽量轻渲染
  • • 先显示纯文本或低成本结构
  • • 等一段稳定后再做较重的 markdown 解析
  • • 代码高亮按块处理,而不是整段反复全量跑

你可以把它理解成一句很朴素的话:最忙的时候,别把最贵的活一起干。


五、第四步:长消息和长列表,要分别处理,不要混成一个问题

很多人一说 SSE 卡,就笼统地说“列表太长”。其实这里至少有两个问题:

1)当前这条消息越来越长

这会导致单条消息本身的渲染成本上升。

2)整个消息列表越来越长

这会导致整个聊天容器越来越重。

这两个问题相关,但不是一回事。

针对第一类,常见要做的是:

  • • 减少当前消息的高频全量重算
  • • 分段渲染
  • • 控制重解析范围

针对第二类,常见要做的是:

  • • 历史消息虚拟化
  • • 超长内容折叠
  • • 非当前区域懒处理
  • • 会话切换时按需恢复

如果你把这两个问题混成一句“做虚拟列表就好了”,面试官一般能听出来你讲得还不够细。

因为很多时候:你正在卡的那条消息,甚至还没进入虚拟列表能救的阶段。


六、第五步:滚动策略本身,就是渲染优化的一部分

这一点很多人会漏。

AI 对话页面最常见的体验问题之一,就是:

  • • 新内容一来就自动滚到底部
  • • 用户刚往上翻又被拽回来
  • • 布局反复抖
  • • 你感觉不是纯卡,但就是很烦

这时候你会发现,性能问题不只是 render 时间。还是滚动联动策略的问题。

所以我一般会把滚动策略也当成优化的一部分来看:

  • • 用户在底部附近时,才自动跟随新内容
  • • 用户已经主动往上翻时,就暂停自动滚动
  • • 只有在合适的时机再恢复跟随
  • • 尽量减少每次追加内容都触发滚动测量和位置修正

这一步做完,很多时候用户的主观感受会明显改善。因为页面不再像一直在跟人抢控制权。

所以说到底:渲染性能优化,不只是让 CPU 轻一点,也是让交互别一直打架。


七、第六步:状态边界拆清楚,比无脑加 memo 更有效

这点我很认同。

很多 SSE 页面后面越写越乱,不是因为框架不行。而是因为状态全搅在一起了。

比如:

  • • 当前流式文本
  • • 整个消息数组
  • • 会话级 loading
  • • 停止生成状态
  • • 错误状态
  • • 引用区展开状态
  • • 输入框状态

这些如果全塞在一层里,流一更新,整页都跟着晃。

这时候你再拼命上 memo,收益其实有限。因为根上就是:状态传播范围太大。

所以更有效的思路往往是:

  • • 把当前流状态单独拆出来
  • • 把历史列表状态稳定住
  • • 把输入区和会话区解耦
  • • 把不相关的 UI 状态独立出去

这样做的好处不是“看起来更高级”。而是你真的能减少那些本来不该发生的更新。

很多性能问题,到最后都不是“优化技巧不够多”。而是“系统边界一开始就没画清楚”。


八、如果面试官问你:你具体做过哪些优化动作?

这时候别只讲理念。最好给出一套像项目里真的做过的动作组合。

比如可以这样答:

1)先做 buffer 合并更新

避免 token 级别直接刷 UI。

2)把当前流式消息单独维护

不要每次 append 都刷新整个历史消息数组。

3)流式阶段先轻渲染

markdown、代码高亮做延后或分阶段处理。

4)代码块按块处理

不要每次新片段进来都对全文重新高亮。

5)长列表按需虚拟化或折叠

特别是历史很长的时候。

6)优化滚动跟随策略

让自动滚动只在用户需要的时候发生。

7)拆状态边界

避免一次流更新牵动整页。

你这样答,面试官更容易感受到:这不是你临时拼出来的答案。而像是你真的把问题压过一轮。


九、面试里可以怎么组织成一段顺口回答?

你可以这么答:

我做 SSE 渲染性能优化时,核心思路不是阻止流式更新,而是控制更新频率、缩小更新影响范围,并且把高成本渲染链路从高频阶段拆出去。

然后接着讲:

比如我会先做 buffer 合并,避免每个 token 都直接触发 UI 更新;再把当前正在生成的消息和历史消息隔离,避免整个 messageList 跟着频繁重渲染;对于 markdown 和代码高亮这种重链路,会做分阶段处理,不在最频繁更新的时候全量执行;另外还会配合滚动策略和状态拆分,减少布局抖动和无效更新。

最后收一句:

SSE 优化不是单点技巧,而是让流式更新只影响该影响的那一小块。


小团子总结

SSE 渲染优化,很多时候不是“别让它动”。而是“它可以动,但别每动一下都惊动全场”。