AI 流式返回的 Markdown 内容时,遇到的渲染性能和闪烁问题
前端在展示 AI 流式返回的 Markdown 内容时,遇到的渲染性能和闪烁问题,根源在于传统的 Markdown 渲染器是为处理完整文档设计的,并不适应“逐字追加”的流式场景。
每当收到一个新 token(字符块),常规做法(如 react-markdown)是将整个文本重新解析并重新渲染整个 DOM 树。当流式传输速率达到 100 token/秒时,就意味着每秒进行 100 次完整的重渲染,这必然会导致性能开销巨大、界面卡顿和明显的视觉闪烁。
要解决这个问题,核心思路是从“全量重绘”转向“增量更新”。以下是几种主流的解决方案。
方案一:使用专为流式场景设计的渲染库(推荐)
这是最直接、高效的方案。市面上已经有一些优秀的库,它们从底层解决了流式渲染的痛点。
| 库名称 | 适用框架 | 核心特点 |
|---|---|---|
stream-md | React 19, Next.js | 增量块解析,仅重新渲染当前活跃块;推测性内联闭合,避免闪烁。闭包块通过 React.memo 冻结。体积小(约10KB)。 |
@janhq/streamdown | React | 可直接替代 react-markdown。优雅处理不完整的 Markdown 块,支持 GFM、代码高亮、KaTeX、Mermaid 等。 |
streaming-markdown-react | React | 三层记忆化架构(容器→块→组件)。提供 useSmoothStream 防止破坏代码块结构。 |
markstream-vue | Vue 3 | 零闪烁,内存可预测。提供虚拟窗口(长文档)和增量批处理(打字机效果)两种模式。 |
incremark-renderer | 框架无关 | 基于 marked.js,检测稳定的块边界,仅重新解析新增部分。可生成 DOM 补丁进行局部更新。 |
使用示例(React + stream-md):
import { StreamMD } from 'stream-md';
import 'stream-md/styles.css';
function Chat({ text }) {
// 只需传入累积的完整文本,库内部会自动进行差异对比和增量更新
return <StreamMD text={text} theme="dark" />;
}
方案二:手动实现分块渲染
如果不依赖第三方库,可以自己实现一个轻量级的增量渲染策略。核心思想是将流式文本拆分成独立的“块”(Block),并分别为每个块进行渲染。
- 定义块结构:将 Markdown 解析为一种更适合增量更新的中间数据结构。例如,每个块可以包含类型(
type)、内容(content)和是否完整(isComplete)等信息。 - 实现增量解析逻辑:
- 识别稳定块:已闭合的段落、完整的代码块等,标记为“稳定”。
- 保留可变尾块:未完成的代码块、表格等,作为“可变尾块”单独处理。
- 局部更新:当新内容到达时,只重新解析和渲染“可变尾块”,而“稳定块”保持不变。
- 结合框架能力优化:
- React:使用
React.memo包裹“稳定块”组件,阻止其不必要的重渲染。 - Vue:利用
v-once指令或合理的key值来复用和稳定已渲染的 DOM 元素。 - 批量更新:使用
Vue.nextTick或 React 的unstable_batchedUpdates将多个更新合并到一次渲染中。
- React:使用
方案三:通用性能优化技巧
无论使用哪种方案,以下技巧都能进一步提升体验:
- 节流/防抖:对于极高频率的更新(如 100 token/秒),可以使用
requestAnimationFrame或设置最小延迟(如minDelay={12})来限制渲染帧率,避免过度占用主线程。 - 虚拟滚动:当对话历史很长时,使用
react-window或vue-virtual-scroller等库只渲染可视区域内的内容,大幅减少 DOM 节点数量。 - 代码高亮优化:对于代码块,将其高亮计算放到 Web Worker 中执行,避免阻塞主线程。或者,只在代码块完整接收后进行一次高亮。
- 检查浏览器扩展:某些浏览器扩展(如自动翻译)可能会破坏流式渲染的 DOM 结构,导致异常。在开发时,可以尝试在无痕模式下测试以排除此类干扰。
总结
解决流式 Markdown 渲染问题的关键在于停止全量重渲染,拥抱增量更新。
| 方案 | 优点 | 缺点 |
|---|---|---|
| 专用库 | 开箱即用,性能最优,维护成本低 | 可能需要学习新 API,对定制化支持有限 |
| 手动分块 | 高度可控,可定制 | 实现复杂,需要处理各种边界情况 |
| 通用优化 | 普适性强,可叠加使用 | 只能缓解,不能根治问题 |
对于大多数项目,首选方案是直接使用一个专为流式场景设计的成熟库,如 stream-md、@janhq/streamdown 或 markstream-vue。这能让你用最少的投入,获得最专业的流式渲染体验。