返回文章列表

Markdown 引擎对比:marked / markdown-it / remark 怎么选

Markdown 引擎对比:marked / markdown-it / remark 怎么选

JavaScript / Node.js 生态里有三个主流的 Markdown 引擎,下载量都是百万级:

  • marked — 2011 年问世,最早的纯 JS Markdown 解析器之一
  • markdown-it — 2014 年,强调 100% CommonMark 兼容 + 插件系统
  • remark — 2014 年,unified 生态的核心,AST-first

它们都能把 # Hello 转成 <h1>Hello</h1>,但在性能、可定制性、错误处理上差异巨大。我们的 Markdown 转 HTML 工具 选用了 marked + DOMPurify 的组合——下文讲讲为什么。

三种引擎的世界观

marked — 字符串到字符串

import { marked } from "marked";
const html = marked.parse("# Hello\n\nWorld");
// '<h1>Hello</h1>\n<p>World</p>\n'

API 简单到极致:进 Markdown 字符串,出 HTML 字符串。中间没有 AST、没有 token、没有 visitor pattern。需要扩展时用 marked.use({ renderer }) 重写 HTML 输出函数:

marked.use({
  renderer: {
    heading(text, level) {
      const id = text.toLowerCase().replace(/\s+/g, "-");
      return `<h${level} id="${id}">${text}</h${level}>`;
    },
  },
});

markdown-it — 字符串 → token 流 → 字符串

import MarkdownIt from "markdown-it";
const md = new MarkdownIt();

// 一步到位
const html = md.render("# Hello");

// 或者拿到中间 token 流自己玩
const tokens = md.parse("# Hello", {});
// [{ type: 'heading_open', tag: 'h1', ... },
//  { type: 'inline', children: [{ content: 'Hello' }] },
//  { type: 'heading_close', tag: 'h1', ... }]

token 流是扁平数组(不是树)。插件通过 md.use(plugin) 注册,可以修改 token 流或注入新的 render 规则。

remark — 字符串 → AST → AST → 字符串

import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";

const file = await unified()
  .use(remarkParse)
  .use(remarkRehype)
  .use(rehypeStringify)
  .process("# Hello");
const html = String(file);

remark 把 Markdown 解析成 MDAST(Markdown AST),可以经过任意多个 transformer 处理,最后转成 HAST(HTML AST)再 stringify。这是最 functional 的设计,但也最 verbose——简单 case 要写 4 个 import。

性能对比

实测把一篇 50KB 的 Markdown 文档(约 1 万词)转 HTML,三个引擎的耗时(M1 MacBook,Node 22):

引擎单次耗时内存峰值
marked4ms6MB
markdown-it12ms9MB
remark(最小配置)38ms24MB
remark + GFM + smartypants65ms35MB

marked 是最快的,差距能到 10 倍。原因是它跳过了中间 AST 表示——边解析边输出字符串。这也是它扩展性最差的根本:没有 AST 就没法做"路过的处理器",所有定制都要 hook 进 render 函数。

对静态生成(SSG)来说 10 倍差距不要紧——一个站点几十篇文章总耗时也就几秒。但如果你做的是用户实时输入预览(笔记应用、Notion 类工具),4ms vs 65ms 是肉眼可感的:

  • 4ms:每次按键都能刷新预览,零延迟感
  • 65ms:用户连续打字时会感到 UI 卡顿(特别是 Markdown 文档变长时)

我们的工具页是后者场景——用户左侧改 Markdown 右侧实时渲染,所以 marked 是更稳的选择。

CommonMark 兼容性

CommonMark 是 2014 年提出的 Markdown 形式化规范——核心目的是消除"同一段 Markdown 在不同引擎下输出不同"的混乱。

引擎CommonMark 兼容性GFM 支持
marked~98%内置
markdown-it100%(创立目的)插件 markdown-it-gfm-alerts 等
remark100%remark-gfm 插件

marked 的 2% 偏差大部分是边缘 case(嵌套列表的 lazy continuation、HTML block 边界判定等),日常使用基本碰不到。如果你的产品要导入用户从 GitHub / GitLab 复制过来的 Markdown,markdown-it 或 remark 的 100% 兼容更安全。

GFM(GitHub Flavored Markdown)扩展包括:

  • 表格
  • 任务列表 - [x]
  • 删除线 ~~text~~
  • URL 自动识别
  • 围栏代码块的语言标识

marked 内置全部 GFM,开箱即用;markdown-it / remark 需要单独装插件。

安全性:HTML 注入与 XSS

这是 Markdown 引擎选型最容易被忽视、但最严重的问题

Markdown 规范允许内联 HTML:

段落里有一段 <strong>HTML</strong><script>alert('XSS')</script>

如果你直接 dangerouslySetInnerHTML={{ __html: marked.parse(userInput) }},那段 script 标签就执行了。

三个引擎的默认行为:

  • marked:直接输出 HTML(包括 <script><iframe><img onerror>
  • markdown-it:构造函数有 html: false 选项,默认 不解析 内联 HTML(把 < 转义为 &lt;
  • remark:默认 不解析 内联 HTML(行为最保守)

markdown-it 和 remark 的默认安全是优势,但仍然不够——它们对 Markdown 自带的语法没有过滤能力:

[点我](javascript:alert('XSS'))

这个链接在所有三个引擎里默认都会生成 <a href="javascript:...">。Chrome 等现代浏览器拦截 javascript: 协议,但旧浏览器和某些 webview 不拦。

结论:任何处理用户输入的 Markdown 引擎都必须配 sanitizer。最常见的搭配:

import { marked } from "marked";
import DOMPurify from "dompurify";

const html = DOMPurify.sanitize(marked.parse(userInput));

DOMPurify 是事实标准——OWASP 推荐,jsdom 兼容(可在 SSR 用),白名单可定制。我们的工具页就是这个组合:marked 出原始 HTML,DOMPurify 清洗,再渲染。

remark 生态有自己的 sanitizer(rehype-sanitize),思路类似但 API 更 verbose。markdown-it 没有官方 sanitizer,社区方案是用 markdown-it 自己的 validateLink hook 拦协议白名单——但功能不如 DOMPurify 完整。

扩展性对比

要给 Markdown 加自定义语法(如 :::warning 警告框、@mention[[wikilink]]),三个引擎的难度差异:

任务markedmarkdown-itremark
改 heading 渲染加 anchor⭐ 重写 renderer.heading⭐⭐ 加 plugin 改 token⭐⭐⭐ 写 visitor + transformer
加新 inline 语法(@mention)⭐⭐ 用 extensions API⭐⭐ 标准 inline rule API⭐⭐ remark 自定义 micromark extension
加新 block 语法(::: 块)⭐⭐⭐ 复杂⭐⭐ markdown-it-container 插件⭐⭐ remark-directive 插件
AST 后处理(如自动加 footnote)❌ 不支持🟡 token 数组操作✅ AST visitor 天生合适

简单替换 = marked 最快;复杂语法扩展 + 多步处理 = remark 最干净;中间地带 = markdown-it 最 productive。

生态系统

社区周边工具的丰富度:

remark / unified 生态最大

  • MDX(Markdown + JSX)建立在 remark 之上
  • 静态站点生成器 Gatsby / Astro / Docusaurus 默认 remark
  • VS Code Markdown lint、Prettier Markdown 都用 remark
  • 1000+ 插件覆盖各种场景

markdown-it 生态成熟稳定

  • Vue / Vite 默认 Markdown 处理用 markdown-it
  • VuePress、VitePress、Nuxt Content 都基于 markdown-it
  • 插件不如 remark 多,但常用的都有

marked 生态小但够用

  • highlight.js / prism.js 代码高亮容易接
  • 没有大型框架默认用 marked(因为可扩展性弱)
  • 多用于"嵌入式 Markdown 渲染"场景

选型决策树

用 marked 当你

  • 性能是第一优先(实时预览、移动端)
  • 需求简单:渲染 + 代码高亮 + GFM
  • 配合 DOMPurify 处理安全
  • 不需要做语法扩展或 AST 后处理

用 markdown-it 当你

  • 用 Vue / Vite / VuePress / VitePress 生态
  • 需要修改部分语法行为(自定义容器、链接处理)
  • 性能要够好(中等档位)
  • token 流操作能满足扩展需求

用 remark / unified 当你

  • 用 Next.js / MDX / Astro / Gatsby
  • 需要多步骤 AST 处理(如 TOC 生成 + footnote 收集 + smartypants 替换)
  • 团队习惯 functional + 类型系统(remark 有完整 TS 类型)
  • 性能不是瓶颈(静态构建场景)

一些反直觉的建议

  1. 不要在生产环境用 marked.parse(input, { sanitize: true })——marked 4.0 已经移除内置 sanitize,那是个已知缺陷的实现。永远用 DOMPurify 外部处理。

  2. github-markdown-css 是免费午餐——三个引擎的输出 HTML 都能用 github-markdown-css 直接套样式,省去自己写 CSS。

  3. 代码高亮单独选——marked / markdown-it / remark 自己都不带代码高亮。常见组合:highlight.js(更小、自动识别语言)或 prism.js(功能多、token-level 控制)。Shiki 是新选择,体验最好但首次加载需下载 TextMate grammar(~500KB)。

  4. MDX ≠ Markdown——如果你想在 Markdown 里直接写 React 组件(<MyChart data={...} />),那是 MDX,必须用 remark + @mdx-js。这是 marked / markdown-it 都做不到的领域。

总结

维度markedmarkdown-itremark
速度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
体积~50KB~100KB~150KB+
可扩展性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
生态⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
上手难度⭐ 极简⭐⭐ 中等⭐⭐⭐⭐ verbose
CommonMark98%100%100%

没有 silver bullet——选型本质是 "速度 vs 表达力" 的取舍。marked 是"够用就好"的选择,remark 是"我要绝对掌控"的选择,markdown-it 是务实中间路线。

延伸阅读