Web 数学 (MathJax / KaTeX)

浏览器里跑不了 TeX。因此把 LaTeX 数学公式放上网页 的工具——MathJaxKaTeX——都是用 JavaScript 重写的 TeX 数学排版,两者对同一个问题下了相反的赌注。在本机排同样的 500 个公式,KaTeX 用了 45 毫秒,MathJax 用了 264 毫秒。可只看速度选型,往往会在别处栽跟头:两者默认都不把 $...$ 当成数学公式。本页讨论两种设计的差别、你真正会看到的报错文字,以及粘进页面的 TeX 最后还在不在。

KaTeX 与 MathJax 的区别:该用哪一个

公式多、要速度就用 KaTeX;希望写下的 LaTeX 尽可能原样通过就用 MathJax。 速度差别不在于谁优化得更卖力,而在于 API 的形态。KaTeX 的 katex.renderToString(...) 同步返回字符串——调用当场就拿到 HTML,不会事后插入排版结果导致布局跳动。MathJax 在浏览器中则围绕 MathJax.typesetPromise() 设计,顾名思义返回一个 Promise。读者偶尔会瞥见一瞬未渲染的 \frac{1}{2},正是这种异步性的副作用。

html
<!-- MathJax 3: configure BEFORE the script tag loads -->
<script>
  window.MathJax = {
    tex: { inlineMath: [["$", "$"], ["\\(", "\\)"]] }
  };
</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>

<!-- KaTeX: stylesheet, engine, then the auto-render pass -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex/dist/contrib/auto-render.min.js"
        onload="renderMathInElement(document.body);"></script>

$...$ 为何不渲染:KaTeX 源码里的那行注释

两个库默认都不把 $...$ 当作行内公式。这不是疏忽,而是有意为之的默认值。打开 KaTeX auto-render 的源码,$ 那一条就在那里被注释掉了,理由写在紧上面一行:「LaTeX uses $…$, but it ruins the display of normal $ in text」。正文里出现两个货币符号 $,中间的内容就会被当成公式。MathJax 的默认值也一样:inlineMath 只有 \(...\),而 displayMath$$...$$\[...\]。「公式以源码原样显示」这类求助,相当一部分就出在这一点上。

js
// KaTeX auto-render, default delimiters (dist/contrib/auto-render.js):
//   { left: "$$",  right: "$$",  display: true  }
//   { left: "\\(",  right: "\\)",  display: false }
//   { left: "\\[",  right: "\\]",  display: true  }
//   plus \begin{equation} \begin{align} \begin{alignat} \begin{gather} \begin{CD}
//
// and this line is deliberately commented out in the source:
//   // LaTeX uses $...$, but it ruins the display of normal `$` in text:
//   // {left: "$", right: "$", display: false},

// turn it on yourself only if the page has no currency amounts:
renderMathInElement(document.body, {
  delimiters: [
    { left: "$$", right: "$$", display: true },
    { left: "$",  right: "$",  display: false },
    { left: "\\(", right: "\\)", display: false },
    { left: "\\[", right: "\\]", display: true }
  ],
  ignoredTags: ["script", "noscript", "style", "textarea", "pre", "code"]
});

KaTeX 做不到的事,以及它打印的报错文字

KaTeX 支持的是 LaTeX 数学的一个子集,越界的内容会以异常抛出。措辞是固定的,形如 KaTeX parse error: Undefined control sequence: \eqref at position 1:。最致命的缺口是公式编号的交叉引用——\label\eqref 都没有定义,于是要给公式编号并在正文中引用的文档,仅靠 KaTeX 就走不通了。化学式也过不去:\ce{H2O} 需要另行加载 mhchem 扩展。另一个常踩的坑是 KaTeX parse error: {align} can be used only in display mode.,在行内分隔符里写 align 环境时就会出现。

不过「KaTeX 不能用宏」是个误解。\newcommand\def\gdef 都可用;只要给 macros 选项传一个空对象,用 \gdef 定义的内容就会跨调用保留。全站通用的自定义宏,正是用这套机制一次性注入的。反过来看,MathJax 内置了约三十个 TeX 宏包的等价实现——amsamscdmathtoolsmhchemcancelbraketbussproofsempheqcolortbl 等等,这才是「兼容性更广」的实质。另外,若不想让一个坏公式毁掉整页,记住 throwOnError: false:KaTeX 不再抛出异常,而是把出错的源码用红色(#cc0000)原样显示出来。

你写的内容KaTeXMathJax
\frac \int \underbrace \text可用可用
\newcommand \def \gdef可用(借 macros 保留)可用(macros 配置)
\label \eqref不支持——Undefined control sequence可用(由 tags 选项编号)
\ce{H2O}需另加 mhchem 扩展已内置
align (inline){align} can be used only in display mode.可用

MathML 现在能用了吗:2023 年补上的缺口

主流浏览器都能显示 MathML。空缺最久的是 Chrome:支持加进去又撤掉,到版本 109 才正式回归(Edge 同样从 109 起)。Firefox 自版本 2 起就支持,Safari 自 10 起支持。话虽如此,实务中手写 MathML 的场合仍然不多。更要紧的是,两个库都把 MathML 放进了输出。KaTeX 总会在可见的 HTML 旁附上一个 <math> 元素;MathJax 的标准包 tex-mml-chtml.js 从一开始就加载了为辅助技术补充 MathML 的扩展。屏幕阅读器之所以读得出公式,正是靠这一隐藏层。

贴上页面的 TeX 还在吗:从哪一个能把公式复制回来

用 KaTeX 就还在;用 MathJax,默认不在。KaTeX 输出的 MathML 里必定有一个 <annotation encoding="application/x-tex">,里面原封不动地放着原始 TeX。喂给它 x^2+1,输出中某处就仍留着 x^2+1 这串字符。此外发行包里还附带一个 copy-tex 扩展,其源码注释写着「Replace .katex elements with their TeX source」——加载之后,选中公式复制,进入剪贴板的不是一串字形,而是 $x^2+1$。MathJax 常规的 HTML 输出里没有原始 TeX,它给的方案是右键菜单里的「Show Math As」,从那里取回原始写法。

html
<!-- what KaTeX puts in the DOM for x^2+1 -->
<span class="katex"><span class="katex-mathml"><math
    xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow>
  <msup><mi>x</mi><mn>2</mn></msup><mo>+</mo><mn>1</mn>
  </mrow><annotation encoding="application/x-tex">x^2+1</annotation>
</semantics></math></span><span class="katex-html" aria-hidden="true">...</span></span>

<!-- load this and Ctrl-C on a formula yields $x^2+1$, not glyphs -->
<script defer src="https://cdn.jsdelivr.net/npm/katex/dist/contrib/copy-tex.min.js"></script>

把自定义宏送到浏览器

这里正是与「把整篇文档转成 HTML」那类工具的分界线。MathJax 和 KaTeX 收到的都只是数学片段,谁也不会读你的导言区。所以哪怕在 .tex 开头写了 \newcommand{\R}{\mathbb{R}},浏览器一侧也毫无所知,\R 会以未定义的红色出现。宏必须另行通过配置对象传入。转换工具那边也有同样的坑:make4htmathjax 模式把数学以 LaTeX 原样留在 HTML 中,因此自定义宏同样不会展开。转换工具的详情留给「LaTeX → HTML」,但对策是共通的——把宏定义集中在一处,让 TeX 和 JavaScript 都读它

js
// KaTeX: one shared object, and \gdef survives from call to call
const macros = {};
katex.renderToString("\\gdef\\R{\\mathbb{R}}", { macros });
katex.renderToString("f\\colon \\R \\to \\R", { macros });   // \R resolves

// or declare them up front, the same way for the auto-render pass:
renderMathInElement(document.body, {
  macros: { "\\R": "\\mathbb{R}", "\\eps": "\\varepsilon" },
  throwOnError: false          // print the bad source in red, do not break the page
});

// MathJax 3: the equivalent lives in the config object
window.MathJax = {
  tex: {
    macros: { R: "\\mathbb{R}", eps: "\\varepsilon" },
    tags: "ams"                // this is what enables \label and \eqref
  }
};
  • 公式以源码原样显示 → 先怀疑分隔符。$...$ 在 KaTeX 和 MathJax 中默认都是关闭的。
  • 文档用到 \eqref → 选 MathJax,并设置 tags: "ams"。KaTeX 根本没有编号机制。
  • 含数百个公式的列表页 → 选 KaTeX。同步渲染意味着版面不会事后跳动。
  • 有自定义宏 → 一定要通过 macros 传入。你的导言区到不了浏览器。
  • 不想让一个坏公式毁掉整页 → KaTeX 用 throwOnError: false:以红字显示并继续。
  • 想把整篇文档搬上网 → 那不是数学渲染器的活(见「LaTeX → HTML」)。