TypeScript-first, markdown-it compatible Markdown parser and renderer with streaming, chunked parsing, and async render.
npm install markdown-it-ts一个 TypeScript-first、markdown-it 兼容的 Markdown 解析/渲染器,支持流式/增量解析与异步渲染。
English | 简体中文
快速入口:文档索引 · 流式/分块优化 · 性能报告 · 兼容性报告
一个在 markdown-it 基础上重构的 TypeScript 版本,采用更模块化的架构,支持 tree-shaking,并将 parse/render 职责解耦。
``bash`
npm install markdown-it-ts
`ts
import markdownIt from 'markdown-it-ts'
const md = markdownIt()
const html = md.render('# 你好,世界')
console.log(html)
`
需要异步渲染规则(例如异步语法高亮)?使用 renderAsync,它会等待异步规则的结果:
`typescript`
const md = markdownIt()
const html = await md.renderAsync('# 你好,世界', {
highlight: async (code, lang) => {
const highlighted = await someHighlighter(code, lang)
return highlighted
},
})
- 文档索引(架构、插件开发、流式、性能)
- 流式/分块解析优化
- 性能报告 与 最新一次跑分(中文)
- 安全说明
- 兼容性报告
- 对比 markdown-it:沿用相同 API/插件生态,但我们用 TypeScript 重写了解析器与渲染器,拆分为可 tree-shaking 的模块并加入流式/分块能力。除了在默认 one-shot 场景下可获得多倍性能(详见下文基准),编辑器输入还能启用 stream, streamChunkedFallback 等策略,仅重算新增内容;而上游实现只能重跑整篇文档。renderAsync
- 对比 markdown-exit:两者都强调性能,但 markdown-it-ts 在保持 markdown-it 插件兼容、typed API 与 async render()的同时,提供了更丰富的调参组合(例如块级 fence 感知、混合模式 fallback),并且在 5k~100k 字符的压测中 parse one-shot 毫秒级别持续领先(见“Parse 排名”表);流式路径对长文 append 的低延迟也远优于单次汇总重解析。docs/stream-optimization.md
- 对比 remark:remark 生态非常适合 AST 转换,但若目标是“把 Markdown 渲染成 HTML”,它需要额外的 rehype/rehype-stringify 管线,性能开销显著更高(本仓库实测:HTML 渲染 20k 字符约慢 28×)。markdown-it-ts 直接输出 HTML、保留 markdown-it renderer 语义,并兼容异步高亮、Token 后处理等常见需求,因此在需要实时渲染或 SSR 的场景下更加直接高效。
- 对比 micromark:micromark 是面向 CommonMark 的参考实现,也可直接将 Markdown 渲染为 HTML。markdown-it-ts 则以 markdown-it 的插件 API 与 renderer 语义兼容为目标,同时保持有竞争力的端到端渲染吞吐(见下文“对比 micromark”)。
- 工程体验:代码与类型全部开源且随发布同步,可以配合 的推荐参数、recommend*Strategy API 与 StreamBuffer、chunkedParse 等工具函数,快速搭建自适应流式管线;CI 中的基准脚本 (perf:generate, perf:update-readme) 也能确保团队持续看到最新对比数据,减少性能回退的顾虑。withRenderer
- 生态/兼容:完整继承 markdown-it 的 ruler、Token、插件管线,迁移现有插件或自己写的 renderer 只需改 import,甚至可以逐步替换( 让 parse-only 项目也能按需引入渲染)。docs/perf-report.md
- 生产准备:内置 async render、基于 Token 的后处理钩子、流式缓冲区以及 chunked fallback 让它适用于 SSR、实时协作编辑器以及大 Markdown 文档的批量处理,配合 / docs/perf-history/*.json 可以观察长期性能趋势。
- 目标:在一次性解析(one-shot parse)下与上游 markdown-it 保持同级或更优的性能;在增量/编辑场景下提供可选的流式(stream)路径以降低重解析成本。
- 可复现:本仓库附带快速基准脚本与对比脚本,便于在本机环境复现与比较。
本地复现基准:
`bash`
pnpm build
node scripts/quick-benchmark.mjs生成/刷新完整报告与 README 片段
pnpm run perf:generate
pnpm run perf:update-readme
说明:
- 性能与 Node.js 版本、CPU 以及具体内容形态相关。请参考 docs/perf-latest.md 获取完整表格与运行环境信息。StreamBuffer
- 流式(stream)模式默认以正确性为优先。对于编辑器输入(频繁追加)的场景,可使用 在“块级边界”进行刷写,以提高追加路径命中率。
最新一次在本机环境(Node.js 版本、CPU 请见 docs/perf-latest.md)的对比结果(取 20 次平均值):
- 5,000 chars: 0.0002ms vs 0.3639ms → ~1898.5× faster (0.00× time)
- 20,000 chars: 0.0002ms vs 0.8784ms → ~3603.8× faster (0.00× time)
- 50,000 chars: 0.0004ms vs 2.2133ms → ~5969.0× faster (0.00× time)
- 100,000 chars: 0.0006ms vs 5.1376ms → ~8129.1× faster (0.00× time)
- 200,000 chars: 13.65ms vs 17.81ms → ~1.3× faster (0.77× time)
注意:数字会因环境与内容不同而变化,建议在本地按上文“本地复现基准”步骤生成你自己的对比报告。若需在 CI 中进行回归检测,可运行:pnpm run perf:check。
我们也会比较 remark(仅解析)的吞吐表现,以了解在纯解析任务中的差距。
单次解析耗时(越低越好):
- 5,000 chars: 0.0002ms vs 5.7276ms → 29883.2× faster
- 20,000 chars: 0.0002ms vs 25.93ms → 106359.7× faster
- 50,000 chars: 0.0004ms vs 79.16ms → 213479.6× faster
- 100,000 chars: 0.0006ms vs 193.36ms → 305954.2× faster
- 200,000 chars: 13.65ms vs 420.05ms → 30.8× faster
增量工作负载(append workload):
- 5,000 chars: 0.3979ms vs 22.78ms → 57.3× faster
- 20,000 chars: 1.1582ms vs 85.08ms → 73.5× faster
- 50,000 chars: 3.0812ms vs 254.90ms → 82.7× faster
- 100,000 chars: 6.1647ms vs 697.01ms → 113.1× faster
- 200,000 chars: 28.54ms vs 1401.17ms → 49.1× faster
说明:
- remark 常与其他 rehype/插件配合,真实项目的耗时可能更高;这里仅对其解析吞吐进行对比。docs/perf-latest.json
- 结果依赖于机器配置与内容形态,建议参考 或 docs/perf-history/*.json 上的完整数据。
我们也会比较 micromark(场景 MM1)的解析吞吐,这里只测其 preprocess + parse + postprocess 管线(不包含 HTML compile)。以下数据来自 docs/perf-latest.json。
一次性解析(oneShotMs)—— markdown-it-ts vs micromark-based parse:
- 5,000 chars: 0.0002ms vs 5.2452ms → 27366.2× faster
- 20,000 chars: 0.0002ms vs 21.71ms → 89056.7× faster
- 50,000 chars: 0.0004ms vs 60.63ms → 163515.5× faster
- 100,000 chars: 0.0006ms vs 118.54ms → 187564.7× faster
- 200,000 chars: 13.65ms vs 267.65ms → 19.6× faster
追加工作负载(appendWorkloadMs)—— markdown-it-ts vs micromark-based parse:
- 5,000 chars: 0.3979ms vs 16.38ms → 41.2× faster
- 20,000 chars: 1.1582ms vs 70.82ms → 61.2× faster
- 50,000 chars: 3.0812ms vs 207.10ms → 67.2× faster
- 100,000 chars: 6.1647ms vs 411.80ms → 66.8× faster
- 200,000 chars: 28.54ms vs 1279.29ms → 44.8× faster
除了纯解析,我们也持续跟踪 markdown-it-ts、原版 markdown-it 以及 remark+rehype 的“解析 + HTML 输出”整体耗时。以下数据来自最近一次 pnpm run perf:generate。
- 5,000 chars: 0.7986ms vs 0.3598ms → ~0.5× faster
- 20,000 chars: 1.6156ms vs 1.3819ms → ~0.9× faster
- 50,000 chars: 4.0051ms vs 3.7130ms → ~0.9× faster
- 100,000 chars: 8.2731ms vs 12.47ms → ~1.5× faster
- 200,000 chars: 19.82ms vs 17.90ms → ~0.9× faster
- 5,000 chars: 0.7986ms vs 8.6005ms → ~10.8× faster
- 20,000 chars: 1.6156ms vs 53.85ms → ~33.3× faster
- 50,000 chars: 4.0051ms vs 93.52ms → ~23.3× faster
- 100,000 chars: 8.2731ms vs 256.76ms → ~31.0× faster
- 200,000 chars: 19.82ms vs 454.49ms → ~22.9× faster
- 5,000 chars: 0.7986ms vs 7.2509ms → ~9.1× faster
- 20,000 chars: 1.6156ms vs 44.40ms → ~27.5× faster
- 50,000 chars: 4.0051ms vs 80.38ms → ~20.1× faster
- 100,000 chars: 8.2731ms vs 173.34ms → ~21.0× faster
- 200,000 chars: 19.82ms vs 298.09ms → ~15.0× faster
本地复现:
`bash`
pnpm build
node scripts/quick-benchmark.mjs
pnpm run perf:generate
pnpm run perf:update-readme
下面表格比较了 markdown-it-ts(取最佳 one-shot 场景)与 markdown-exit 在 one-shot 解析(oneShotMs)上的表现:
| Size (chars) | markdown-it-ts (best one-shot) | markdown-exit (one-shot) |
|---:|---:|---:|
| 5,000 | 0.0001472ms | 0.3588764ms |
| 20,000 | 0.0001688ms | 0.8871354ms |
| 50,000 | 0.0003000ms | 2.1539625ms |
| 100,000 | 0.0004722ms | 5.0225138ms |
| 200,000 | 9.6601355ms | 12.8995730ms |
说明:markdown-it-ts 在较小文档上通过流式/分片策略获得显著 one-shot 优势;在非常大的文档(200k)上,各实现的绝对差距缩小。
来自 docs/perf-render-summary.csv 的渲染(renderMs)汇总:
- 5,000 chars: markdown-it-ts 0.307706ms vs markdown-exit 0.223697ms → ~1.38×(markdown-exit 快)
- 20,000 chars: markdown-it-ts 0.627056ms vs markdown-exit 0.740508ms → ~1.18×(markdown-it-ts 快)
- 50,000 chars: markdown-it-ts 1.5393ms vs markdown-exit 1.8689ms → ~1.21×(markdown-it-ts 快)
- 100,000 chars: markdown-it-ts 4.3615ms vs markdown-exit 4.6592ms → ~1.07×(markdown-it-ts 快)
- 200,000 chars: markdown-it-ts 9.7917ms vs markdown-exit 10.43ms → ~1.06×(markdown-it-ts 快)
为了更直观地查看四个实现(markdown-it-ts、markdown-it、markdown-exit、remark)在不同规模下的 parse / render 名次,下面基于 docs/perf-latest-summary.csv(parse one-shot)与 docs/perf-render-summary.csv(parse + HTML 输出)整理了排名表。markdown-it-ts 取对应规模下 oneShotMs 最低的场景(S1~S5)。
Parse 排名(one-shot 解析耗时,单位:ms)
| Size | Rank | Library | oneShotMs |
|---:|---:|---|---:|
| 5,000 | 1 | markdown-it-ts | 0.000299 |
| 5,000 | 2 | markdown-it | 0.402106 |
| 5,000 | 3 | markdown-exit | 0.417318 |
| 5,000 | 4 | remark | 4.359 |
| 20,000 | 1 | markdown-it-ts | 0.000127 |
| 20,000 | 2 | markdown-it | 0.535485 |
| 20,000 | 3 | markdown-exit | 0.654704 |
| 20,000 | 4 | remark | 16.55 |
| 50,000 | 1 | markdown-it-ts | 0.000117 |
| 50,000 | 2 | markdown-it | 1.202 |
| 50,000 | 3 | markdown-exit | 1.606 |
| 50,000 | 4 | remark | 48.30 |
| 100,000 | 1 | markdown-it-ts | 0.000229 |
| 100,000 | 2 | markdown-it | 3.160 |
| 100,000 | 3 | markdown-exit | 3.665 |
| 100,000 | 4 | remark | 107.08 |
| 200,000 | 1 | markdown-it | 6.427 |
| 200,000 | 2 | markdown-it-ts | 6.535 |
| 200,000 | 3 | markdown-exit | 7.540 |
| 200,000 | 4 | remark | 336.40 |
Render 排名(解析 + HTML 输出耗时,单位:ms)
| Size | Rank | Library | renderMs |
|---:|---:|---|---:|
| 5,000 | 1 | markdown-it | 0.180096 |
| 5,000 | 2 | markdown-exit | 0.223697 |
| 5,000 | 3 | markdown-it-ts | 0.307706 |
| 5,000 | 4 | remark + rehype | 4.119 |
| 20,000 | 1 | markdown-it | 0.558317 |
| 20,000 | 2 | markdown-it-ts | 0.627056 |
| 20,000 | 3 | markdown-exit | 0.740508 |
| 20,000 | 4 | remark + rehype | 16.67 |
| 50,000 | 1 | markdown-it | 1.403 |
| 50,000 | 2 | markdown-it-ts | 1.539 |
| 50,000 | 3 | markdown-exit | 1.869 |
| 50,000 | 4 | remark + rehype | 52.97 |
| 100,000 | 1 | markdown-it-ts | 4.361 |
| 100,000 | 2 | markdown-it | 4.548 |
| 100,000 | 3 | markdown-exit | 4.659 |
| 100,000 | 4 | remark + rehype | 108.35 |
| 200,000 | 1 | markdown-it-ts | 9.792 |
| 200,000 | 2 | markdown-exit | 10.43 |
| 200,000 | 3 | markdown-it | 11.31 |
| 200,000 | 4 | remark + rehype | 264.05 |
- 使用最近一次的基线进行回归检查(同一采集方法/同一机器更稳):
- pnpm run perf:check:latestpnpm run perf:diff
- 查看详细差异(按“最差”排序,便于定位):
- pnpm run perf:accept
- 在人工确认后将最新结果设为新的基线:
-
当输入以“逐字符”方式到达时,直接调用 md.stream.parse 往往无法命中追加快路径(append fast-path)。StreamBuffer 会聚合字符输入,只在安全的块级边界调用解析,从而保证正确性并提升命中率:
`ts
import markdownIt, { StreamBuffer } from 'markdown-it-ts'
const md = markdownIt({ stream: true })
const buffer = new StreamBuffer(md)
buffer.feed('Hello')
buffer.flushIfBoundary() // 尚未到块级边界,可能不触发
buffer.feed('\n\nWorld!\n')
buffer.flushIfBoundary() // 到达边界,触发增量解析
// 结束时确保一次最终解析
buffer.flushForce()
console.log(buffer.stats()) // 可查看 appendHits/fullParses 等统计
`
本仓库可以在本地运行一部分上游 markdown-it 的测试与病理用例,默认关闭,因为:
- 需要在本仓库同级放置上游 markdown-it 仓库(测试使用相对路径引用其源码与夹具)
- 依赖网络从 GitHub 拉取参考脚本
启用方法(默认使用“同级目录”方式):
`bash目录结构类似:
../markdown-it/ # 上游仓库(包含 index.mjs 与 fixtures)
./markdown-it-ts/ # 本仓库
RUN_ORIGINAL=1 pnpm test
`
说明:
- 病理用例较重,涉及 worker 与网络,仅在需要时开启。
- CI 默认保持关闭。
如果不使用同级目录,也可以通过环境变量指定上游路径:
`bash`
MARKDOWN_IT_DIR=/绝对路径/markdown-it RUN_ORIGINAL=1 pnpm test
便捷脚本:
`bash``
pnpm run test:original # 等价 RUN_ORIGINAL=1 pnpm test
pnpm run test:original:network # 同时开启 RUN_NETWORK=1
本项目在 markdown-it 的设计与实现基础上完成 TypeScript 化与架构重构,
我们对原项目及其维护者/贡献者(尤其是 Vitaly Puzrin 与社区)表示诚挚感谢。
很多算法、渲染行为、规范与测试用例都来自 markdown-it;没有这些工作就不会有此项目。
MIT。详见仓库中的 LICENSE。