大数跨境

@ant-design/x-markdown-mini:面向多端小程序的高性能流式 Markdown 渲染器

@ant-design/x-markdown-mini:面向多端小程序的高性能流式 Markdown 渲染器 支付宝体验科技
2026-07-07
5
导读:x-markdown-mini 的核心设计围绕一个原则展开:流式场景下,已完成的内容不应再承担任何额外开销。

随着大语言模型的普及,流式输出(Streaming)已成为 对话式 AI 应用的标准交互模式:模型逐 Token 生成内容,前端实时增量渲染。此前,我们已面向 Web 端推出了流式友好的 Markdown 渲染器 @ant-design/x-markdown[1]

然而,小程序端缺乏浏览器原生的 HTML 渲染能力,流式 Markdown 渲染会面临格式突变、频繁重解析、性能损耗等问题,我们对现有主流小程序 Markdown 渲染方案进行了研究,发现其在流式场景下存在以下核心局限。

方案
渲染链路
局限性
marked / markdown-it + rich-text
Markdown → HTML → rich-text
交互能力有限,链接、图片预览、动画等支持较弱;支付宝小程序仅支持节点数组,不支持 HTML 字符串。(支付宝文档中心[2])
towxml / mp-html
Markdown → HTML → 节点树 → 渲染
面向静态文档,每次追加内容通常需要重新解析,流式场景易出现性能损耗、格式闪烁。(GitHub[3])
wemark
Markdown → 节点树 → 渲染
已停止维护,难以适配新版小程序生态。(GitHub[4])

流式场景下,上述方案的短板集中在三个瓶颈——转换瓶颈、流式性能瓶颈、易用性瓶颈。下文的三个策略将与之一一对应,逐一拆解:

  1. 转换瓶颈——HTML 中间层的多余开销:现有方案普遍走 "Markdown → HTML → 再解析为节点树" 的链路,HTML 仅充当中间表示,却额外引入一轮解析开销;且 HTML 标签无法直接映射为小程序原生组件,限制了链接点击、图片预览、动画等能力,最终节点往往只是 rich-text 的只读快照。****
  2. 流式性能瓶颈——O(N²) 重复解析 + setData 全量传输:这是流式追加内容时叠加的两道性能损耗
    • 计算侧(O(N²) 重复解析):每当 AI 追加一小段文本,现有方案将全文从 Markdown 重新解析至节点树。若 AI 回答产生 N 个增量片段,累计解析次数为 1+2+3+…+N,总工作量达到 O(N²)。
    • 通信侧(setData 全量传输):小程序采用逻辑层与渲染层分离的架构,每次节点树更新需通过 setData 将完整数据从逻辑层传输至渲染层。节点树越大,单次传输量越大,且传输本身占用主线程,进一步加剧卡顿。
  3. 易用性瓶颈——扩展能力固化,业务难以低成本接入:现有方案的语法与能力大多写死或强耦合——towxml 把公式、代码高亮内置进主包,无法裁剪也无法新增业务语法;mp-html 虽有插件机制,但作用于 HTML / 节点层、面向静态文档;wemark 则已停止维护。业务若想接入 LaTeX 公式、代码高亮,或把 <ant-button><ant-tag> 这类自有业务组件写进正文,往往只能 fork 转换器,或退化为受限的 HTML 标签映射,成本高、易与主干冲突,且难以适配流式与多视图并发。****

前两道瓶颈在短文本下尚不明显,但在 AI 长回答(数千至数万字符)场景下会导致肉眼可见的渲染延迟和交互卡顿;而易用性瓶颈则直接决定了渲染器能否在真实业务中低成本落地。因此我们针对小程序重新设计了一套流式渲染方案:@ant-design/x-markdown-mini[5]

npm i @ant-design/x-markdown-mini

整体架构

x-markdown-mini 的核心设计围绕一个原则展开:流式场景下,已完成的内容不应再承担任何额外开销。整体从上到下分为五层,另有一套扩展机制横向作用于解析与转换:

  • 入口层parse / render / renderNodes 三个 API,默认复用共享实例,并发场景按视图各自 new XMarkdownMini
  • 流式引擎StreamingProcessor 负责稳定块缓存与结尾补全,仅流式路径启用;
  • 解析层:实例隔离的 marked.Lexer,从源文本直接产出语法树 Token[],不经过 HTML 中间表示;
  • 转换层renderTokensToMiniNodes 是唯一的语义映射(无中间 IR),平台差异由 adapter 消化(微信 a[data-href]、支付宝 https 图片 / 丢弃 ol start 等);
  • 渲染层MiniNodeRenderer 组件按 node.name 分发到小程序原生组件,支持微信 / 支付宝双平台;
  • 扩展机制extensions 注册自定义语法、components 注册自定义标签白名单,均实例隔离、不污染全局。

下文的三个策略分别对应上述转换瓶颈、流式性能瓶颈、易用性瓶颈逐一拆解——策略一取消 HTML 中间层降低单次转换开销,策略二以稳定块缓存 + 增量传输同时解决 O(N²) 重复解析与 setData 全量传输,策略三以实例隔离的扩展机制打开低成本接入。

策略一:取消 HTML 中间层,直出小程序节点

现有渲染器普遍采用如下方案:

Markdown → HTML 字符串 → 再次解析为节点树 → 渲染

HTML 仅充当中间表示,却引入了额外的解析开销,并且限制了最终节点的表达能力——HTML 标签无法直接映射为小程序原生组件。

x-markdown-mini 去掉这层中转,直接从语法树拼装节点:

Markdown 字符串 →  MiniNode[] → 渲染

如上图,Markdown 字符串经 marked.lexer 词法分析得到 Token[],再由 renderTokensToMiniNodes 结合平台 adapter 一步产出 MiniNode[] 节点树,最后交给 MiniNodeRenderer 按 node.name 分发到原生组件——全程不经过 HTML。去掉中间层带来两重收益:

  • 性能:减少一轮字符串解析与转换,长文转换速度显著提升;
  • 能力:节点直接对应小程序原生组件(<text><view><image> 等),链接可点击、图片可预览、可叠加动画——而非 rich-text 的只读快照。

策略二:稳定块缓存 + 增量补全 + 增量传输

这是流式场景下最核心的优化,同时解决计算侧(O(N²) 重复解析)与通信侧(setData 全量传输)两道性能损耗

传统方案在每次追加内容后对全文重新解析,本质上假设"全文可能发生变化"。但在流式输出中,已完成的部分是语义稳定的——AI 不会回头修改已经输出的段落。

x-markdown-mini 基于这一观察,每来一段 chunk 就以"代码块外 + 空行收尾"为界扫描出稳定分界,把缓冲切成两段并行处理:

  1. 稳定块(Stable Block):分界之前、已完成的段落,命中缓存直接复用,不再重新解析;
  2. 结尾 / 活跃块(Active Block):AI 仍在输出的尾部片段,可能存在未闭合的语法结构(如 **加粗 缺少闭合 **、代码块未关闭等)。对它做一次 remend 补全(自动闭合未完结的结构),再只重新解析这一小段结尾。

随后把缓存的稳定块节点与新生成的结尾节点合并,仅将增量 patch 通过 onPatch 回调传给渲染层,而非全量 setData 传输整棵节点树。

这一策略同时击破了流式性能瓶颈的两个子问题:

子问题
传统方案
策略二优化
效果
计算侧:O(N²) 重复解析
每次追加后全文重新解析
稳定块缓存 + 结尾增量补全,稳定块只解析一次
累计解析复杂度 O(N²) → O(N)
通信侧:setData 全量传输
每次更新通过 setData 传输完整节点树
onPatch
 只传输增量 patch(新增 / 变更的尾部节点),稳定块节点不参与传输
单次通信数据量与全文长度解耦,从 O(N) 降至 O(增量)

由于稳定块只解析一次、每次增量只补全并重解析尾部一小段,累计解析复杂度从 O(N²) 降到 O(N);同时 onPatch 只传递增量部分,通信开销不再随全文长度线性增长。

策略三:自定义语法/组件,直出节点

不同业务对 Markdown 的诉求不一:有的要 LaTeX 公式,有的要代码高亮、@提及,有的要在正文里塞 <ant-button><ant-tag> 这类业务组件。若渲染器把语法集写死,这些场景只能 fork 转换器——成本高,且容易和主干逻辑冲突。

x-markdown-mini 把扩展做成两个实例隔离的注入点,直接挂在解析 / 转换阶段,不污染全局 marked:

  1. 自定义语法插件(extensions:注册一个 tokenizer 识别新语法,并配一个 miniRenderer(token, ctx) 直接产出 MiniNode。这里延续了策略一的原则——即便是用户扩展,也不产生 HTML,自定义语法同样一步直出小程序节点。内置的 LaTeX、代码高亮插件即以此实现(重依赖按需懒加载,不占主包)。
  1. 自定义组件映射(components:传入一个标签白名单,框架自动合成行内 tokenizer,<ant-button>X</ant-button> 即被解析为名为 ant-button 的 MiniNode,一行配置完成映射;同名用户扩展优先级高于内置合成。

两个注入点都随实例创建而生效、随实例销毁,天然支持并发多视图各自扩展,互不干扰。

易用性:扩展能力横向对比

扩展能力
rich-text
towxml
mp-html
x-markdown-mini
自定义语法扩展
❌ 不支持
⚠️ 语法集写死,新增语法需 fork 源码
⚠️ 有插件机制,但作用于 HTML / 节点层
✅ extensions 注册 tokenizer,直出节点
LaTeX 公式
✅ 内置(耦合在主包)
✅ 插件
✅ 官方插件,按需懒加载不占主包
代码高亮
✅ 内置(耦合在主包)
✅ 插件
✅ 官方插件,按需懒加载不占主包
自定义业务组件(如 <ant-button>
❌ 仅固定标签集
⚠️ 需按 HTML 标签适配,能力有限
✅ components 白名单,一行映射为小程序原生组件
实例隔离 / 不污染全局
❌ 全局配置
❌ 全局配置
✅ 随实例创建销毁,多视图并发互不干扰
扩展是否直出节点(无 HTML 中转)
✅ 扩展同样一步直出小程序节点
流式增量渲染下可用

渲染效果展示

下面是 x-markdown-mini 在微信 / 支付宝真机上的实际渲染录屏,涵盖流式增量渲染LaTeX 公式代码高亮自定义业务组件图片预览 / 链接点击等效果,可直观感受"直出原生节点"带来的交互与动画能力。

实测:越长越快

实验条件

  • 测试对象:x-markdown-mini v1.0、towxml v3.x、mp-html v2.x
  • 测试环境:macOS 26.5.1 / Apple M3Pro,Node.js 20.x
  • 测试方法:同一份 Markdown 源文本,分短文(2.9 KB)、中等(11.6 KB)、长文(29 KB)三档;每档先预热 5 次消除 JIT 影响,再连续执行 20 次取均值;仅测量"Markdown → 可渲染节点树"的纯转换耗时(不含渲染与 setData 传输)
  • 复现方式:完整测试脚本与数据见:benchmark/experiments/2026-06-26-render-parity-results.md[6]

篇幅越长,领先越多:长文比 towxml 快 4.4 倍、比 mp-html 快 2.5 倍。

总结

@ant-design/x-markdown-mini 针对小程序流式 Markdown 渲染在转换、性能和扩展性上的三大瓶颈,提出一套流式友好的多端节点直出架构。通过取消 HTML 中间表示、稳定块缓存与尾部增量补全、插件化节点直出三项策略,实现高性能流式渲染与自定义语法扩展。框架支持微信、支付宝等小程序一套代码多端运行,基准测试下长文本转换性能较 towxml、mp-html 分别提升约 4.4× 和 2.5×

源码与复现:github.com/ant-design/x-markdown-mini[7]

性能数据 benchmark/experiments/2026-06-26-render-parity-results.md[8]

参考资料

[1] 

@ant-design/x-markdown: https://x.ant.design/x-markdowns/introduce-cn

[2] 

支付宝文档中心: https://opendocs.alipay.com/mini/component/rich-text

[3] 

towxml: https://github.com/sbfkcel/towxml

[4] 

wemark: https://github.com/TooBug/wemark

[5] 

@ant-design/x-markdown-mini: https://x-markdown-mini.ant.design/

[6] 

完整测试脚本与数据: https://github.com/ant-design/x-markdown-mini/blob/main/benchmark/experiments/2026-06-26-render-parity-results.md

[7] 

源码与复现: https://github.com/ant-design/x-markdown-mini

[8] 

性能数据: https://github.com/ant-design/x-markdown-mini/blob/main/benchmark/experiments/2026-06-26-render-parity-results.md

【声明】内容源于网络
0
0
支付宝体验科技
1234
内容 254
粉丝 0
支付宝体验科技 1234
总阅读869
粉丝0
内容254