双引擎架构
Incremark 采用双引擎解析系统,这是我们在开发过程中做出的一个重要架构决策。我们希望为用户提供选择的自由:在追求极致性能和追求完美兼容性之间找到最适合自己的平衡点。
为什么需要双引擎?
在开发 Incremark 的过程中,我们面临一个核心问题:如何在流式 AI 场景中实现最佳性能?
经过大量调研和测试,我们发现:
- Marked 解析速度极快,但原生不支持脚注、数学公式等高级特性
- Micromark 规范兼容性完美,插件生态丰富,但包体积较大
最终我们决定:两个都要。
通过双引擎架构,用户可以根据实际场景灵活选择:
- 对性能敏感的 AI 聊天场景 → 使用 Marked 引擎
- 需要严格规范兼容的文档场景 → 使用 Micromark 引擎
引擎概览
| 引擎 | 速度 | 特性 | 包体积 | 最佳场景 |
|---|---|---|---|---|
| Marked(默认) | ⚡⚡⚡⚡⚡ | 标准 + 增强扩展 | 较小 | 实时流式、AI 对话 |
| Micromark | ⚡⚡⚡ | 完整 CommonMark + 插件 | 较大 | 复杂文档、严格规范 |
Marked 引擎(默认)
Marked 引擎是我们的默认选择,专为流式 AI 场景深度优化。
为什么选择 Marked 作为默认引擎?
- 极致的解析速度:Marked 是 JavaScript 生态中最快的 Markdown 解析器之一
- 成熟稳定:拥有超过 10 年的历史,被无数项目验证
- 易于扩展:提供了灵活的扩展机制,我们可以按需增加功能
- 小巧的包体积:有利于前端项目的 tree-shaking 优化
我们为 Marked 做了什么增强?
原生 Marked 是一个"够用就好"的解析器,它专注于标准 Markdown 语法,不包含很多高级特性。但在 AI 场景中,我们经常需要这些特性。
因此,Incremark 通过自定义扩展为 Marked 增加了以下能力:
| 功能 | 原生 Marked | Incremark 增强 | 说明 |
|---|---|---|---|
| 脚注 | ❌ 不支持 | ✅ 完整 GFM 脚注 | [^1] 引用和 [^1]: 内容 定义 |
| 数学公式 | ❌ 不支持 | ✅ 行内和块级公式 | $E=mc^2$ 和 $$...$$ |
| 自定义容器 | ❌ 不支持 | ✅ 指令语法 | :::tip、:::warning、:::danger |
| 内联 HTML 解析 | ⚠️ 仅保留原文 | ✅ 结构化解析 | 将 HTML 解析为可操作的 AST 节点 |
| 乐观引用处理 | ❌ 不支持 | ✅ 流式友好 | 在流式输入时优雅处理未完成的链接/图片 |
| 脚注定义块 | ❌ 不支持 | ✅ 多行内容 | 支持包含代码块、列表的复杂脚注 |
💡 这些扩展是我们针对 AI 场景精心设计的。它们在提供完整功能的同时,尽可能减少性能开销。
使用方式
Marked 引擎是默认的,无需特殊配置:
<script setup>
import { ref } from 'vue'
import { IncremarkContent } from '@incremark/vue'
const content = ref('')
const isFinished = ref(false)
</script>
<template>
<!-- 默认使用 Marked 引擎 -->
<IncremarkContent
:content="content"
:is-finished="isFinished"
/>
</template>启用/禁用特定功能
<template>
<IncremarkContent
:content="content"
:is-finished="isFinished"
:incremark-options="{
gfm: true, // GFM 扩展(表格、删除线等)
math: true, // 数学公式
containers: true, // 自定义容器
htmlTree: true // HTML 结构化解析
}"
/>
</template>Micromark 引擎
Micromark 引擎是追求完美规范兼容性的选择。
为什么提供 Micromark 选项?
尽管 Marked 引擎已经能满足大多数场景,但某些用户可能有更严格的需求:
- 严格的 CommonMark 规范兼容:Micromark 是目前最符合 CommonMark 规范的解析器
- 丰富的插件生态:GFM、Math、Directive 等插件都经过社区长期打磨
- 精确的位置信息:AST 节点包含准确的行列位置,便于错误定位
- 更好的边界情况处理:在一些复杂嵌套场景下表现更稳定
使用方式
要使用 Micromark 引擎,需要导入 MicromarkAstBuilder 并通过 astBuilder 选项传入:
// 在你的 composable 或 setup 中
import { createIncremarkParser } from '@incremark/core'
import { MicromarkAstBuilder } from '@incremark/core/engines/micromark'
const parser = createIncremarkParser({
astBuilder: MicromarkAstBuilder,
gfm: true,
math: true
})注意:
IncremarkContent组件默认使用 Marked 引擎。如需使用 Micromark,你需要直接使用useIncremark配合自定义 parser。
何时应该使用 Micromark?
- 你的内容包含复杂的嵌套结构
- 你需要处理一些 Marked 无法正确解析的边界情况
- 你的应用对 CommonMark 规范兼容性有严格要求
- 你需要使用我们内置扩展之外的 Micromark 插件
完整基准测试数据
我们对 38 个真实的 Markdown 文件进行了基准测试,以下是完整的测试结果:
测试环境
- 测试文件:38 个文件,共 6,484 行,128.55 KB
- 测试方式:模拟流式输入,逐字符 append
- 对比方案:Streamdown、markstream-vue、ant-design-x
完整测试结果
| 文件名 | 行数 | 大小(KB) | Incremark | Streamdown | markstream | ant-design-x | vs Streamdown | vs markstream | vs ant-design-x |
|---|---|---|---|---|---|---|---|---|---|
| test-footnotes-simple.md | 15 | 0.09 | 0.3 ms | 0.0 ms | 1.4 ms | 0.2 ms | 0.1x | 4.7x | 0.6x |
| simple-paragraphs.md | 16 | 0.41 | 0.9 ms | 0.9 ms | 5.9 ms | 1.0 ms | 1.1x | 6.7x | 1.2x |
| test-footnotes-multiline.md | 21 | 0.18 | 0.6 ms | 0.0 ms | 2.2 ms | 0.4 ms | 0.1x | 3.5x | 0.6x |
| test-footnotes-edge-cases.md | 27 | 0.25 | 0.8 ms | 0.0 ms | 4.2 ms | 1.2 ms | 0.0x | 5.3x | 1.5x |
| test-footnotes-complex.md | 28 | 0.24 | 2.1 ms | 0.0 ms | 4.8 ms | 1.0 ms | 0.0x | 2.3x | 0.5x |
| introduction.md | 34 | 1.57 | 5.6 ms | 12.6 ms | 75.6 ms | 12.8 ms | 2.2x | 13.4x | 2.3x |
| devtools.md | 51 | 0.92 | 1.2 ms | 0.9 ms | 6.1 ms | 1.1 ms | 0.8x | 5.0x | 0.9x |
| footnotes.md | 52 | 0.94 | 1.7 ms | 0.2 ms | 10.6 ms | 1.9 ms | 0.1x | 6.3x | 1.2x |
| html-elements.md | 55 | 1.02 | 1.6 ms | 2.2 ms | 12.6 ms | 2.8 ms | 1.4x | 7.8x | 1.7x |
| themes.md | 58 | 0.96 | 1.9 ms | 1.3 ms | 8.6 ms | 1.8 ms | 0.7x | 4.4x | 0.9x |
| test-footnotes-comprehensive.md | 63 | 0.66 | 5.6 ms | 0.1 ms | 25.8 ms | 7.7 ms | 0.0x | 4.6x | 1.4x |
| auto-scroll.md | 72 | 1.68 | 3.9 ms | 3.5 ms | 39.9 ms | 4.9 ms | 0.9x | 10.1x | 1.2x |
| custom-codeblocks.md | 72 | 1.44 | 3.4 ms | 2.0 ms | 14.9 ms | 2.5 ms | 0.6x | 4.4x | 0.7x |
| custom-components.md | 73 | 1.40 | 4.0 ms | 2.0 ms | 32.7 ms | 2.9 ms | 0.5x | 8.1x | 0.7x |
| custom-containers.md | 88 | 1.67 | 4.2 ms | 2.4 ms | 18.1 ms | 3.1 ms | 0.6x | 4.3x | 0.7x |
| typewriter.md | 88 | 1.89 | 5.6 ms | 4.1 ms | 35.0 ms | 4.9 ms | 0.7x | 6.2x | 0.9x |
| concepts.md | 91 | 4.29 | 12.0 ms | 50.5 ms | 381.9 ms | 53.6 ms | 4.2x | 31.9x | 4.5x |
| INLINE_CODE_UPDATE.md | 94 | 1.66 | 4.7 ms | 17.2 ms | 60.9 ms | 15.6 ms | 3.7x | 12.9x | 3.3x |
| comparison.md | 109 | 5.39 | 20.5 ms | 74.0 ms | 552.2 ms | 85.2 ms | 3.6x | 26.9x | 4.1x |
| basic-usage.md | 130 | 3.04 | 8.5 ms | 12.3 ms | 74.1 ms | 14.1 ms | 1.4x | 8.7x | 1.7x |
| CODE_BACKGROUND_SEPARATION.md | 131 | 2.83 | 8.7 ms | 28.8 ms | 153.6 ms | 31.3 ms | 3.3x | 17.6x | 3.6x |
| P2_SUMMARY.md | 138 | 2.61 | 8.3 ms | 38.4 ms | 157.2 ms | 41.9 ms | 4.6x | 18.9x | 5.0x |
| quick-start.md | 146 | 3.04 | 7.3 ms | 7.3 ms | 64.2 ms | 9.6 ms | 1.0x | 8.8x | 1.3x |
| complex-html-examples.md | 147 | 3.99 | 9.0 ms | 58.8 ms | 279.3 ms | 57.2 ms | 6.6x | 31.1x | 6.4x |
| CODE_COLOR_SEPARATION.md | 162 | 3.51 | 10.0 ms | 32.8 ms | 191.1 ms | 36.9 ms | 3.3x | 19.1x | 3.7x |
| P0_OPTIMIZATION_REPORT.md | 168 | 3.53 | 10.1 ms | 56.2 ms | 228.0 ms | 58.1 ms | 5.6x | 22.6x | 5.8x |
| COLOR_SYSTEM_REFACTOR.md | 169 | 3.78 | 18.5 ms | 64.0 ms | 355.5 ms | 69.1 ms | 3.5x | 19.2x | 3.7x |
| FOOTNOTE_TEST_GUIDE.md | 219 | 2.87 | 12.3 ms | 0.2 ms | 167.6 ms | 45.0 ms | 0.0x | 13.7x | 3.7x |
| P2_COLORS_PACKAGE_REPORT.md | 226 | 4.10 | 11.4 ms | 77.9 ms | 311.6 ms | 80.5 ms | 6.8x | 27.2x | 7.0x |
| FOOTNOTE_FIX_SUMMARY.md | 236 | 3.93 | 22.7 ms | 0.5 ms | 535.0 ms | 120.8 ms | 0.0x | 23.6x | 5.3x |
| BASE_COLORS_SYSTEM.md | 259 | 4.47 | 35.8 ms | 43.0 ms | 191.8 ms | 43.4 ms | 1.2x | 5.4x | 1.2x |
| OPTIMIZATION_COMPARISON.md | 270 | 5.42 | 17.8 ms | 52.3 ms | 366.1 ms | 61.9 ms | 2.9x | 20.6x | 3.5x |
| P1_OPTIMIZATION_REPORT.md | 327 | 5.63 | 20.7 ms | 106.8 ms | 433.8 ms | 114.8 ms | 5.2x | 21.0x | 5.5x |
| OPTIMIZATION_PLAN.md | 371 | 6.89 | 33.1 ms | 67.6 ms | 372.1 ms | 76.7 ms | 2.0x | 11.2x | 2.3x |
| OPTIMIZATION_SUMMARY.md | 391 | 6.24 | 19.1 ms | 208.4 ms | 980.6 ms | 217.8 ms | 10.9x | 51.3x | 11.4x |
| P1.5_COLOR_SYSTEM_REPORT.md | 482 | 9.12 | 22.0 ms | 145.5 ms | 789.8 ms | 168.2 ms | 6.6x | 35.9x | 7.7x |
| BLOCK_TRANSFORMER_ANALYSIS.md | 489 | 9.24 | 75.7 ms | 574.3 ms | 1984.1 ms | 619.9 ms | 7.6x | 26.2x | 8.2x |
| test-md-01.md | 916 | 17.67 | 87.7 ms | 1441.1 ms | 5754.7 ms | 1656.9 ms | 16.4x | 65.6x | 18.9x |
| 【合计】 | 6484 | 128.55 | 519.4 ms | 3190.3 ms | 14683.9 ms | 3728.6 ms | 6.1x | 28.3x | 7.2x |
如何理解这份数据?
我们诚实地告诉你:有些场景 Incremark 更慢
你可能注意到,在 test-footnotes-*.md 和 FOOTNOTE_*.md 这些文件上,Incremark 比 Streamdown 慢很多(0.0x - 0.1x)。
原因很简单:Streamdown 不支持脚注语法。
当 Streamdown 遇到 [^1] 这样的脚注引用时,它直接跳过不处理。而 Incremark 会:
- 识别脚注引用
- 解析脚注定义块(可能包含多行内容、代码块、列表等)
- 建立引用关系
- 生成正确的 AST 结构
这不是性能问题,是功能差异。我们认为完整的脚注支持对于 AI 场景非常重要,所以选择实现它。
真正的性能优势在哪里?
排除脚注相关的文件,看看标准 Markdown 内容的表现:
| 文件 | 行数 | Incremark | Streamdown | 优势 |
|---|---|---|---|---|
| concepts.md | 91 | 12.0 ms | 50.5 ms | 4.2x |
| comparison.md | 109 | 20.5 ms | 74.0 ms | 3.6x |
| complex-html-examples.md | 147 | 9.0 ms | 58.8 ms | 6.6x |
| P0_OPTIMIZATION_REPORT.md | 168 | 10.1 ms | 56.2 ms | 5.6x |
| OPTIMIZATION_SUMMARY.md | 391 | 19.1 ms | 208.4 ms | 10.9x |
| test-md-01.md | 916 | 87.7 ms | 1441.1 ms | 16.4x |
结论:对于标准 Markdown 内容,文档越大,Incremark 的优势越明显。
为什么会有这样的差距?
这是 O(n) vs O(n²) 算法复杂度的直接体现。
传统解析器(Streamdown、ant-design-x、markstream-vue)每次接收新的 chunk 都要重新解析整个文档:
Chunk 1: 解析 100 字符
Chunk 2: 解析 200 字符 (100 旧 + 100 新)
Chunk 3: 解析 300 字符 (200 旧 + 100 新)
...
Chunk 100: 解析 10,000 字符总工作量: 100 + 200 + 300 + ... + 10000 = 5,050,000 次字符处理
Incremark 的增量解析只处理新内容:
Chunk 1: 解析 100 字符 → 缓存稳定块
Chunk 2: 仅解析 ~100 新字符
Chunk 3: 仅解析 ~100 新字符
...
Chunk 100: 仅解析 ~100 新字符总工作量: 100 × 100 = 10,000 次字符处理
差距是 500 倍。这就是为什么 18KB 的文档能快 16 倍以上。
功能对等性
我们努力确保两个引擎在功能上保持一致:
| 功能 | Marked 引擎 | Micromark 引擎 |
|---|---|---|
| GFM(表格、删除线、自动链接) | ✅ | ✅ |
数学公式($...$ 和 $$...$$) | ✅ | ✅ |
自定义容器(:::tip 等) | ✅ | ✅ |
| HTML 元素解析 | ✅ | ✅ |
| 脚注 | ✅ | ✅ |
| 打字机动画 | ✅ | ✅ |
| 增量更新 | ✅ | ✅ |
切换引擎
引擎选择是在初始化时进行的,而非运行时。这是为了 tree-shaking 优化而设计的。
为什么不支持运行时切换?
为了确保最优的打包体积:
- 默认导入只包含
marked引擎 micromark引擎需要单独导入- 这让打包工具可以 tree-shake 未使用的引擎
如何切换引擎
<script setup>
import { ref } from 'vue'
import { IncremarkContent } from '@incremark/vue'
const content = ref('')
const isFinished = ref(false)
// 引擎在初始化时选择
// 使用 marked(默认):无需额外导入
// 使用 micromark:导入 MicromarkAstBuilder
</script>
<template>
<!-- 默认使用 marked 引擎 -->
<IncremarkContent
:content="content"
:is-finished="isFinished"
:incremark-options="{ gfm: true, math: true }"
/>
</template>使用 Micromark 引擎
要使用 micromark,从单独的引擎入口导入 MicromarkAstBuilder:
import { createIncremarkParser } from '@incremark/core'
import { MicromarkAstBuilder } from '@incremark/core/engines/micromark'
// 使用 micromark 引擎创建解析器
const parser = createIncremarkParser({
astBuilder: MicromarkAstBuilder,
gfm: true,
math: true
})⚠️ Tree-shaking 说明:从
@incremark/core/engines/micromark导入只会将 micromark 添加到你的 bundle 中。默认导入只保留 marked。
扩展引擎
两个引擎都支持自定义扩展。详见 扩展指南。
// 自定义 marked 扩展示例
import { createCustomExtension } from '@incremark/core'
const myExtension = createCustomExtension({
name: 'myPlugin',
// ... 扩展配置
})总结与建议
| 方面 | Marked | Micromark |
|---|---|---|
| 解析速度 | ⚡⚡⚡⚡⚡ | ⚡⚡⚡ |
| 包体积 | 📦 更小 | 📦 较大 |
| CommonMark 兼容性 | ✅ 良好 | ✅ 完美 |
| 内置扩展 | ✅ 脚注、数学公式、容器 | ✅ 通过插件 |
| 插件生态 | 🔧 成长中 | 🔧 成熟 |
| 推荐场景 | 流式 AI、实时渲染 | 静态文档、严格规范 |
我们的建议:
- 大多数场景:使用默认的 Marked 引擎,它已经足够好
- 遇到解析问题:如果某些边界情况 Marked 处理不好,尝试切换到 Micromark
- 极端性能需求:Marked 引擎是你的最佳选择
- 严格规范需求:Micromark 引擎更适合你
我们会持续优化两个引擎,确保它们都能为你提供最好的体验。