Incremark 0.3.0 发布:双引擎架构 + 完整插件生态,AI 流式渲染的终极方案

从 O(n²) 到 O(n):为 AI 时代打造的流式 Markdown 渲染器

如果你开发过 AI 聊天应用,你可能注意到一个令人沮丧的问题:对话越长,渲染越卡

原因很简单——每次 AI 输出新的 token,传统 markdown 解析器都会从头开始重新解析整个文档。这是一个根本性的架构问题,而且随着 AI 输出越来越长,问题只会越来越严重。

我们开发了 Incremark 来解决这个问题。

2025 年 AI 的残酷现实

如果你一直关注 AI 的发展,你会发现数据变得越来越夸张:

  • 2022:GPT-3.5 的回复?几百个字,问题不大
  • 2023:GPT-4 把输出提升到 2,000-4,000 字
  • 2024-2025:推理模型(o1、DeepSeek R1)输出 10,000+ 字的"思考过程"

我们正在从 4K token 的对话走向 32K,甚至 128K。没人谈论的一个事实是:渲染 500 字和渲染 50,000 字的 Markdown 是完全不同的工程问题。

大多数 markdown 库?它们是为博客文章设计的,不是为会"大声思考"的 AI 设计的。

为什么你的 Markdown 解析器在骗你

当你通过传统解析器流式传输 AI 输出时,底层发生了什么:

text
复制代码
Chunk 1: 解析 100 字符 ✓ Chunk 2: 解析 200 字符 (100 旧 + 100 新) Chunk 3: 解析 300 字符 (200 旧 + 100 新) ... Chunk 100: 解析 10,000 字符 😰

总工作量:100 + 200 + 300 + ... + 10,000 = 5,050,000 字符操作。

这是 O(n²)。成本不是线性增长——而是爆炸式增长

对于 20KB 的 AI 回复,这意味着:

  • ant-design-x:1,657 ms 解析时间
  • markstream-vue:5,755 ms(将近 6 秒的解析!)

而这些都是流行的、维护良好的库。问题不在于代码写得不好——而在于架构选择错误。

核心洞察

关键在这里:

一旦一个 markdown 块"完成",它就永远不会改变。

想想看。当 AI 输出:

markdown
复制代码
# 标题 这是一个段落。

在第二个空行之后,这个段落就完成了。锁定了。无论后面来什么——代码块、列表、更多段落——这个段落永远不会再被动了。

那我们为什么要重复解析它 500 次?

Incremark 的工作原理

我们围绕这个洞察构建了 Incremark。核心算法:

  1. 检测稳定边界 — 空行、新标题、代码块结束符
  2. 缓存已完成的块 — 永不再动
  3. 只重新解析待处理的块 — 当前正在接收输入的那个
text
复制代码
Chunk 1: 解析 100 字符 → 缓存稳定块 Chunk 2: 只解析 ~100 新字符 Chunk 3: 只解析 ~100 新字符 ... Chunk 100: 只解析 ~100 新字符

总工作量:100 × 100 = 10,000 字符操作。

这是 500 倍的减少。每个字符最多只被解析一次。这就是 O(n)。

完整基准测试数据

测试环境

  • 测试文件:38 个文件,共 6,484 行,128.55 KB
  • 测试方式:模拟流式输入,逐字符 append
  • 测试数据:真实 AI 对话、文档、代码分析报告(非合成数据)
  • 对比方案:Streamdown、markstream-vue、ant-design-x

完整测试结果

文件名行数大小(KB)IncremarkStreamdownmarkstreamant-design-xvs Streamdownvs markstreamvs ant-design-x
test-footnotes-simple.md150.090.3 ms0.0 ms1.4 ms0.2 ms0.1x4.7x0.6x
simple-paragraphs.md160.410.9 ms0.9 ms5.9 ms1.0 ms1.1x6.7x1.2x
test-footnotes-multiline.md210.180.6 ms0.0 ms2.2 ms0.4 ms0.1x3.5x0.6x
test-footnotes-edge-cases.md270.250.8 ms0.0 ms4.2 ms1.2 ms0.0x5.3x1.5x
test-footnotes-complex.md280.242.1 ms0.0 ms4.8 ms1.0 ms0.0x2.3x0.5x
introduction.md341.575.6 ms12.6 ms75.6 ms12.8 ms2.2x13.4x2.3x
devtools.md510.921.2 ms0.9 ms6.1 ms1.1 ms0.8x5.0x0.9x
footnotes.md520.941.7 ms0.2 ms10.6 ms1.9 ms0.1x6.3x1.2x
html-elements.md551.021.6 ms2.2 ms12.6 ms2.8 ms1.4x7.8x1.7x
themes.md580.961.9 ms1.3 ms8.6 ms1.8 ms0.7x4.4x0.9x
test-footnotes-comprehensive.md630.665.6 ms0.1 ms25.8 ms7.7 ms0.0x4.6x1.4x
auto-scroll.md721.683.9 ms3.5 ms39.9 ms4.9 ms0.9x10.1x1.2x
custom-codeblocks.md721.443.4 ms2.0 ms14.9 ms2.5 ms0.6x4.4x0.7x
custom-components.md731.404.0 ms2.0 ms32.7 ms2.9 ms0.5x8.1x0.7x
custom-containers.md881.674.2 ms2.4 ms18.1 ms3.1 ms0.6x4.3x0.7x
typewriter.md881.895.6 ms4.1 ms35.0 ms4.9 ms0.7x6.2x0.9x
concepts.md914.2912.0 ms50.5 ms381.9 ms53.6 ms4.2x31.9x4.5x
INLINE_CODE_UPDATE.md941.664.7 ms17.2 ms60.9 ms15.6 ms3.7x12.9x3.3x
comparison.md1095.3920.5 ms74.0 ms552.2 ms85.2 ms3.6x26.9x4.1x
basic-usage.md1303.048.5 ms12.3 ms74.1 ms14.1 ms1.4x8.7x1.7x
CODE_BACKGROUND_SEPARATION.md1312.838.7 ms28.8 ms153.6 ms31.3 ms3.3x17.6x3.6x
P2_SUMMARY.md1382.618.3 ms38.4 ms157.2 ms41.9 ms4.6x18.9x5.0x
quick-start.md1463.047.3 ms7.3 ms64.2 ms9.6 ms1.0x8.8x1.3x
complex-html-examples.md1473.999.0 ms58.8 ms279.3 ms57.2 ms6.6x31.1x6.4x
CODE_COLOR_SEPARATION.md1623.5110.0 ms32.8 ms191.1 ms36.9 ms3.3x19.1x3.7x
P0_OPTIMIZATION_REPORT.md1683.5310.1 ms56.2 ms228.0 ms58.1 ms5.6x22.6x5.8x
COLOR_SYSTEM_REFACTOR.md1693.7818.5 ms64.0 ms355.5 ms69.1 ms3.5x19.2x3.7x
FOOTNOTE_TEST_GUIDE.md2192.8712.3 ms0.2 ms167.6 ms45.0 ms0.0x13.7x3.7x
P2_COLORS_PACKAGE_REPORT.md2264.1011.4 ms77.9 ms311.6 ms80.5 ms6.8x27.2x7.0x
FOOTNOTE_FIX_SUMMARY.md2363.9322.7 ms0.5 ms535.0 ms120.8 ms0.0x23.6x5.3x
BASE_COLORS_SYSTEM.md2594.4735.8 ms43.0 ms191.8 ms43.4 ms1.2x5.4x1.2x
OPTIMIZATION_COMPARISON.md2705.4217.8 ms52.3 ms366.1 ms61.9 ms2.9x20.6x3.5x
P1_OPTIMIZATION_REPORT.md3275.6320.7 ms106.8 ms433.8 ms114.8 ms5.2x21.0x5.5x
OPTIMIZATION_PLAN.md3716.8933.1 ms67.6 ms372.1 ms76.7 ms2.0x11.2x2.3x
OPTIMIZATION_SUMMARY.md3916.2419.1 ms208.4 ms980.6 ms217.8 ms10.9x51.3x11.4x
P1.5_COLOR_SYSTEM_REPORT.md4829.1222.0 ms145.5 ms789.8 ms168.2 ms6.6x35.9x7.7x
BLOCK_TRANSFORMER_ANALYSIS.md4899.2475.7 ms574.3 ms1984.1 ms619.9 ms7.6x26.2x8.2x
test-md-01.md91617.6787.7 ms1441.1 ms5754.7 ms1656.9 ms16.4x65.6x18.9x
【合计】6484128.55519.4 ms3190.3 ms14683.9 ms3728.6 ms6.1x28.3x7.2x

诚实面对:我们慢的地方

你会注意到数据中有些奇怪的地方。对于 footnotes.mdFOOTNOTE_FIX_SUMMARY.md,Streamdown 看起来快得多:

文件IncremarkStreamdown原因
footnotes.md1.7 ms0.2 msStreamdown 不支持脚注
FOOTNOTE_FIX_SUMMARY.md22.7 ms0.5 ms同上——它直接跳过了

这不是性能问题——这是功能差异。

当 Streamdown 遇到 [^1] 脚注语法时,它直接忽略。Incremark 完整实现了脚注——而且我们必须解决一个流式场景特有的棘手问题:

在流式场景中,引用通常比定义先到达

text
复制代码
Chunk 1: "详见脚注[^1]..." // 引用先到达 Chunk 2: "更多内容..." Chunk 3: "[^1]: 这是脚注定义" // 定义后到达

传统解析器假设你有完整的文档。我们构建了"乐观引用"机制,在流式传输过程中优雅地处理不完整的链接/图片,然后在定义到达时解析它们。

我们选择完整实现脚注、数学公式块($...$)和自定义容器(:::tip),因为这些是真实 AI 内容所需要的。

我们真正的优势

排除脚注文件,看看标准 markdown 的性能:

文件行数IncremarkStreamdown优势
concepts.md9112.0 ms50.5 ms4.2x
comparison.md10920.5 ms74.0 ms3.6x
complex-html-examples.md1479.0 ms58.8 ms6.6x
OPTIMIZATION_SUMMARY.md39119.1 ms208.4 ms10.9x
test-md-01.md91687.7 ms1441.1 ms16.4x

规律很明显:文档越大,我们的优势越大。

对于最大的文件(17.67 KB),Incremark 的优势最为明显:

  • vs Streamdown:快 16.4 倍
  • vs ant-design-x:快 18.9 倍
  • vs markstream-vue:快 65.6 倍

为什么差距这么大?

这就是 O(n) vs O(n²) 的实际表现。

传统解析器每次收到新 chunk 都重新解析整个文档:

text
复制代码
Chunk 1: 解析 100 字符 Chunk 2: 解析 200 字符 (100 旧 + 100 新) Chunk 3: 解析 300 字符 (200 旧 + 100 新) ... Chunk 100: 解析 10,000 字符

总工作量:100 + 200 + ... + 10,000 = 5,050,000 字符操作。

Incremark 只处理新内容:

text
复制代码
Chunk 1: 解析 100 字符 → 缓存稳定块 Chunk 2: 只解析 ~100 新字符 Chunk 3: 只解析 ~100 新字符 ... Chunk 100: 只解析 ~100 新字符

总工作量:100 × 100 = 10,000 字符操作。

这是 500 倍的差距。而且随着文档增长,差距只会更大。

什么时候用 Incremark

适合使用 Incremark 的场景:

  • AI 聊天流式输出(Claude、ChatGPT 等)
  • 长篇 AI 内容(推理模型、代码生成)
  • 实时 markdown 编辑器
  • 需要脚注、数学公式或自定义容器的内容
  • 100K+ token 的对话

⚠️ 考虑使用其他方案的场景:

  • 一次性静态 markdown 渲染(直接用 marked 就行)
  • 非常小的文件(<500 字符)——开销不值得

双引擎,一个目标

Marked 还是 Micromark? 两者各有取舍。

Marked 极快但缺少高级功能。Micromark 规范完美但更重。

我们的答案:两个都支持。

引擎速度最佳场景
Marked(默认)⚡⚡⚡⚡⚡实时流式、AI 对话
Micromark⚡⚡⚡复杂文档、严格 CommonMark

我们用自定义 tokenizer 扩展了 Marked,支持脚注、数学公式和容器。如果遇到 Marked 无法处理的边界情况,只需一个配置就能切换到 Micromark。

两个引擎产生完全相同的 mdast 输出。你的渲染代码不关心底层用的是哪个引擎。

没人谈论的打字机问题

你知道 ChatGPT 那种丝滑的"打字"效果吗?大多数实现是这样做的:

ts
复制代码
displayText = fullText.slice(0, currentIndex)

这会不断破坏 markdown。你会看到渲染到一半的 **粗体** 标签、闪烁的代码块、看起来像喝醉了的语法。

我们把动画移到了 AST 层。我们的 BlockTransformer 理解结构——它在节点内部做动画,永远不会跨节点。结果:丝滑流畅的打字效果,同时尊重 markdown 语义。

跨框架支持

我们深知前端生态的多样性。Incremark 提供开箱即用的框架适配:

框架包名版本要求
Vue@incremark/vueVue 3.5+
React@incremark/reactReact 18+
Svelte@incremark/svelteSvelte 5+

一个核心,三个框架,零行为差异。

所有框架共享:

  • 完全一致的 API 设计
  • 相同的组件结构和 DOM 输出
  • 统一的主题系统(@incremark/theme
  • 相同的性能特性
bash
复制代码
# 选择你的框架 npm install @incremark/vue npm install @incremark/react npm install @incremark/svelte

Vue 示例

vue
复制代码
<script setup> import { ref } from 'vue' import { IncremarkContent } from '@incremark/vue' const content = ref('') const isFinished = ref(false) async function handleStream(stream) { for await (const chunk of stream) { content.value += chunk } isFinished.value = true } </script> <template> <IncremarkContent :content="content" :is-finished="isFinished" :incremark-options="{ gfm: true, math: true }" /> </template>

React 示例

tsx
复制代码
import { useState } from 'react' import { IncremarkContent } from '@incremark/react' function Chat() { const [content, setContent] = useState('') const [isFinished, setIsFinished] = useState(false) async function handleStream(stream: AsyncIterable<string>) { for await (const chunk of stream) { setContent(prev => prev + chunk) } setIsFinished(true) } return ( <IncremarkContent content={content} isFinished={isFinished} incremarkOptions={{ gfm: true, math: true }} /> ) }

Svelte 示例

svelte
复制代码
<script> import { IncremarkContent } from '@incremark/svelte' let content = $state('') let isFinished = $state(false) async function handleStream(stream) { for await (const chunk of stream) { content += chunk } isFinished = true } </script> <IncremarkContent {content} {isFinished} incremarkOptions={{ gfm: true, math: true }} />

下一步

这是 0.3.0 版本。我们才刚刚开始。

AI 世界正在走向更长的输出、更复杂的推理轨迹、更丰富的格式。传统解析器跟不上——它们的 O(n²) 架构注定如此。

我们开发 Incremark 是因为我们自己需要它。希望你也觉得它有用。


📚 文档incremark.com 💻 GitHubkingshuaishuai/incremark 🎮 在线演示Vue | React | Svelte

如果这篇文章帮你节省了调试时间,去 GitHub 点个 ⭐️ 吧。有问题?开个 issue 或者在下面留言。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
王一帅
下载 APP