Markdown 预解析:别等全文完了再渲染,如何流式增量渲染代码块和公式?

传统的 Markdown 渲染逻辑(如 marked(fullText))在 AI 流式输出面前就是个"性能黑洞"。

如果每来一个 Token 就把几千字的全文重绘一遍,不仅 CPU 会因为重复的正则匹配而爆表 ,更糟糕的是 UI 体验 :由于解析器还没看到结尾的 ``````````` 或 $$,代码块和公式会频繁地在"纯文本"和"渲染态"之间反复横跳(Flicker)

要做到"流式且无损",我们需要一套状态机驱动的增量预解析方案


1. 核心原理:解析器的"状态化"

传统的解析器是"无状态"的。而流式解析器需要记住: "我现在处于什么环境?"

我们可以将解析过程拆解为三个核心状态:

  1. TEXT:常规文本模式。
  2. CODE :检测到 ```````````,进入代码块模式。
  3. MATH :检测到 $$[,进入公式模式。

2. 代码实现:StreamMarkdownRenderer 逻辑

核心思路是:在闭合标签到达之前,先"假装"它已经闭合了,并创建一个占位节点进行增量更新。

JavaScript

kotlin 复制代码
class StreamMarkdownRenderer {
  constructor(container) {
    this.container = container;
    this.currentState = 'TEXT';
    this.activeNode = null; // 当前正在被填充的 DOM 节点
    this.buffer = '';       // 未处理的碎片缓存
  }

  push(token) {
    this.buffer += token;
    this.parse();
  }

  parse() {
    // 逻辑简化:检测 buffer 中的特殊标识符
    if (this.buffer.includes('```') && this.currentState === 'TEXT') {
      this.currentState = 'CODE';
      this.activeNode = this.createCodeBlock();
      this.buffer = this.buffer.split('```')[1]; // 移除标识符
    } 
    else if (this.buffer.includes('$$') && this.currentState === 'TEXT') {
      this.currentState = 'MATH';
      this.activeNode = this.createMathBlock();
      this.buffer = this.buffer.split('$$')[1];
    }

    this.updateActiveNode();
  }

  updateActiveNode() {
    if (!this.activeNode) {
      // 普通文本追加
      this.renderText(this.buffer);
      this.buffer = '';
      return;
    }

    // 增量填充代码或公式
    if (this.currentState === 'CODE') {
      this.activeNode.querySelector('code').textContent += this.buffer;
      // 触发高亮(如 Prism.highlightElement)
      this.highlight(this.activeNode);
    } 
    else if (this.currentState === 'MATH') {
      // 增量渲染 KaTeX
      this.renderKaTeX(this.activeNode, this.buffer);
    }
    this.buffer = '';
  }
}

3. 针对不同类型的进阶处理

① 代码块:语法高亮的"预热"

在流式状态下,Shiki 虽然美观但太重,Prism.js 是更好的选择。

  • 技巧 :不要在每个字符进来时都调用 highlight。利用我们之前聊过的 requestAnimationFrame,每隔 50ms 左右进行一次重排高亮,这样既能保证"语法变色",又不会卡死主线程。

② 数学公式:KaTeX 的"平滑修正"

公式渲染最怕的是 a + b = c a + b = c a+b=c 还没打完,公式就显示报错红叉。

  • 技巧 :使用 try-catch 包裹 katex.render。如果当前内容不完整导致报错,则先以纯文本样式显示内容,直到检测到完整的公式语法。

E = m c 2 E = mc^2 E=mc2

(这是公式渲染态)


4. 3 个"防坑"策略

  1. "假闭合"处理

    AI 偶尔会断网或中断,导致 ``````````` 永远不出现。你的 Renderer 必须有一个 Auto-Close 机制:如果解析器进入 CODE 状态后 5 秒没有新内容,自动强行闭合它。

  2. 避免 HTML 标签截断

    如果你在流式渲染中允许 HTML(如 <br>),一定要小心 Token 刚好把 <br> 劈成 <br> 的情况。策略 :如果 buffer 的结尾包含 <& 等起始符,先憋住不发,等下一个 Token 凑整。

  3. 计算属性的"懒加载"

    对于公式和代码块,不要频繁查询 scrollHeight。在大规模 AI 响应中,频繁的布局查询会触发浏览器的同步布局震荡(Layout Thrashing)。


5. 效果对比

方案 刷新率 稳定性 CPU 消耗
全量重绘 随 Token 频率 极差 (代码块反复闪烁) O ( N 2 ) O(N^2) O(N2),字越多越卡
增量预解析 随屏幕刷新率 极高 (平滑增长) O ( 1 ) O(1) O(1) 或 O ( N ) O(N) O(N),恒定稳定

相关推荐
程序员黑豆6 小时前
Java类型推断完全指南:从var到菱形运算符,掌握使用限制与最佳实践
java·前端·ai编程
To_OC7 小时前
踩了个 TS 的坑之后,我终于把 type 和 interface 掰明白了
前端·react.js·typescript
GreenTea7 小时前
深度解读 Anthropic 多智能体报告:更强的模型 ≠ 更好的协调
前端·后端·算法
浮生望8 小时前
前端API工程化:用 Mock 数据与 Axios 配置实现独立于后端的并行开发
前端
万少8 小时前
给 DeepSeek Harness 装个"应用商店":一条命令,595 个插件随你逛
前端·javascript·后端
波波0078 小时前
C# 15 重磅新特性: 带标签 break 与 continue:重新定义嵌套循环控制流
服务器·前端·c#
Brown.alexis9 小时前
es6知识点1-自备使用
前端·ecmascript·es6
小爬的老粉丝10 小时前
JavaScript 纯前端预览 WPS:先识别容器,再路由解析器
前端·javascript·wps
计算机魔术师10 小时前
新兴多智能体系统的模式与问题
前端
用户0595401744611 小时前
AI Agent 上下文污染踩坑实录:用 pytest + Redis 揪出 3 类记忆串号 bug,排查 6 小时
前端·css