この記事 では数式を書いているのですが、なんと Mac の Kyenote で TeX の数式を入力し、画像にして貼り付けていました。

さすがにこれを続けるのはダサいので、MathJaxKaTeX のどちらかを採用すべく、jekyll-spaceship を含めて比較してみました。また JekTex というプラグインも見つけたので、まとめてレポートしたいと思います。

比較のポイント

比較のポイントは以下の2つです。

FOUCの例
FOUC - Flash Of Unstyled Content
  • ページの表示速度と、FOUC(Flash Of Unstyled Content、外部スタイルシートの読み込み前に、ブラウザのデフォルトスタイルで一時的に表示されてしまう事象)の影響

  • 想定するユースケース(例えば数式が主体のコンテンツだけでなく、数式の入力方法や書式を説明するコンテンツなど)での使い勝手

1.表示速度と FOUC の評価

それぞれのスクリプトが推奨する <script> の設置方法で、ほぼ同内容のコンテンツを表示するページを作成し、ブラウザの開発者ツールPageSpeed Insights で評価しました。「デスクトップ」の指標はほぼ満点となるため、「携帯電話」の指標を参照しています。以下は評価結果の一覧です。理由は後述しますが、JekTex (css:preload) がほぼ満点となりました。

表中の指標
スクリプト 設置方法 PFM FCP [sec] LCP [sec] SPI [sec] TBT [ms] FOUC
MathJax js:async 80 1.0 1.0 1.1 860 ×
^ js:async/defer なし 77 4.0 4.1 4.0 50 ×
KaTeX js:defer 84 1.5 1.5 1.5 610
^ css:preload + js:defer 93 1.5 2.5 1.5 220
JekTex css:preload 99 1.5 1.8 1.5 0

以下は、各設置方法に対応したサンプルページと詳細です。参考にして下さい。

MathJax (js:async)

MathJax では、async 属性を付けた <script><head> 内に設置する事が 推奨されています。これにより表示速度は上がりますが、ページのレンダリングが早期に始まり、また TBT も長いため、FOUC が目立ちます。

<script async id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
MathJaxの読み込みタイムライン MathJaxのページスピード結果(指標) MathJaxのページスピード結果(診断)

MathJax (js:defer/async なし)

非同期読み込みの属性を付けない <script><head> 内に設置する、jekyll-spaceship デフォルトのパターンです。スクリプトの読み込みと解析が完了するまで初期のレンダリングがブロックされますが、TBT が短く、FOUC がやや目立ちにくいパターンと言えます。

また 対応ブラウザは少ない ですが、defer blocking="render' とすると、さらに FOUC が目立たなくなりますが、PFM が悪化するので推奨できません。

MathJax-blockingの読み込みタイムライン MathJax-blockingのページスピード結果(指標) MathJax-blockingのページスピード結果(診断)

KaTeX (js:defer)

KaTeX推奨パターン では、defer 属性を付けた2つの <script><head> に配置します。わざわざ Document:DOMContentLoaded を待つまでもなく、2つ目の <script>onload から renderMathInElement() を呼び出してレンダリングを開始します。

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" integrity="sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js" integrity="sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js" integrity="sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk" crossorigin="anonymous" onload="renderMathInElement(document.body);"></script>
KaTeXの読み込みタイムライン KaTeXのページスピード結果(指標) KaTeXのページスピード結果(診断)

KaTeX (css:preload + js:defer)

もう一つの推奨パターン として、<link>rel="preload" を付与することが提案されています。

どうせならということで、Web フォントも先読みし、さらに jsdelivr の結合機能 で2つのスクリプトを1つにしたサンプルを作成しました。大抵のブラウザの TCP 同時接続数 以下で、並列に読み込む事が可能になります。

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" integrity="sha256-fl23kUuX5ZajbBq7Z8zH8XT4u3HTjJqIxVsmLtFzf5c=" crossorigin="anonymous">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/fonts/KaTeX_Main-Regular.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/fonts/KaTeX_Math-Italic.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/fonts/KaTeX_Size2-Regular.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/fonts/KaTeX_Size1-Regular.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/combine/npm/katex@0.16.10,npm/katex@0.16.10/dist/contrib/auto-render.min.js" onload="renderMathInElement(document.body);"></script>
KaTeX-preloadの読み込みタイムライン KaTeX-preloadのページスピード結果(指標) KaTeX-preloadのページスピード結果(診断)

jekyll-spaceship

jekyll-spaceship は、コンテンツの作成はこれ1本で大抵は事足りると思えるほど、機能てんこ盛りのプラグインです。数式を記述したページにだけスクリプトを設置してくれるので、書くことに集中できます。

逆に設置の自由度がないとも言えますが、敢えて MathJax の推奨パターン を採用していないのは、表示速度と FOUC とのバランスを考えてのこと(?)かもしれません。

その表示速度と FOUC については、MathJax (js:defer/async なし) を参照してください。

JekTex (css:preload)

JekTex は、KaTeX 同等のレンダリングエンジンを持つ Jekyll プラグインです。その特徴は何と言っても、数式をサーバーサイドでレンダリングするという点です。このため、ブラウザに JavaScript を読み込む必要がなく、表示速度と FOUC の点で他を圧倒しています。

スタイルシート katex.min.css を読み込む必要はあるので、どうせなら Web フォントも rel="preload" してしまいましょう。

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" integrity="sha256-fl23kUuX5ZajbBq7Z8zH8XT4u3HTjJqIxVsmLtFzf5c=" crossorigin="anonymous">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/fonts/KaTeX_Main-Regular.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/fonts/KaTeX_Math-Italic.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/fonts/KaTeX_Size2-Regular.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/fonts/KaTeX_Size1-Regular.woff2" as="font" type="font/woff2" crossorigin="anonymous">
JekTexの読み込みタイムライン JekTexのページスピード結果(指標) JekTexのページスピード結果(診断)

2.使い勝手の評価

前章のサンプルページは全て、数式そのものを表示するだけでなく、数式の入力方法や書式を説明することを想定ユースケースとしています。ここでは各方式の差異を抽出してみました。

MathJax vs KaTeX

MathJax と KaTeX とでは、それぞれの多少の方言があり、どちらも一長一短あります。次の表は、サンプルページの作成で判明した相違へのリンクです。

スクリプト 相違1 相違2
MathJax \begin{}…\end{}のディスプレイ数式 \begin{}…\end{}のインライン数式
KaTeX \begin{}…\end{}のディスプレイ数式 \begin{}…\end{}のインライン数式

MathJax は LaTeX でエラーと定義されているパターンも受容するのに対し、KaTeX は LaTeX に準拠しようと努力している(完璧ではないという意味)感じです。

また、あまりユースケースが思い浮かびませんが、数式を文字列として JavaScript の変数に代入する際の 注意事項 が MathJax の公式ドキュメントに記載されています。

var math = '\\frac{1}{\\sqrt{x^2 + 1}}';
MathJaxのコンテキストメニュー

さらに MathJax 特有の機能として、数式を右クリックして表示されるコンテキストメニューで、数式のコピーや、Web フォントから SVG への変換など、幾つかの付加的な機能を挙げることができます。拡張機能をインストールすれば、音声読み上げなどもできるようです。

KaTeX vs JekTex

同じ KaTeX 系の JekTex ですが、以下の点でスクリプト版の KaTeX との相違が生じています。

  • <code> タグ内の \[...\]\(...\) を数式に変換してしまう(相違1、相違2)
    この問題を issue に上げたところ、kramdown との関係で正規表現の組み立てに苦労しているものの、解決に向けた検討をしてくれることになりました。期待しましょう。

  • \begin{}...\end{}$$...$$ で囲う必要がある(相違3)
    スクリプト版 KaTeX の場合、\begin{}...\end{} を「空行と $$...$$ で囲む必要がある場合(MathJax vs KaTeX 参照)」と「必要ない場合」があり、一貫性に欠けますが、JekTex の場合、\begin{}...\end{} は必ず $$...$$ で囲う必要があります。

スクリプト 相違1 相違2 相違3
KaTeX \[…\] \(…\) \begin{}…\end{}のディスプレイ数式
JekTex \[…\] \(…\) \begin{}…\end{}のディスプレイ数式

jekyll-spaceship の場合

数式の書式を説明するユースケースで問題が起きました。例えば `...`{% highlight tex %}...{% endhighlight %} といった <code> タグ内の $$\$\$ に変換されてしまいます。この回避には $${% raw %} を適用する必要があり、ちょっと面倒です。

総評

本サイトの場合、受け継ぐ過去の資産もなく、これから数式を書くことがあるかも知れない、という程度なので、JekTex 一択で良いということが分かりました。

一方、従来から KaTeX を使用する Jekyll サイトでは、すぐに JekTex に移行するのは難しいかも知れません。ただ、ページの表示速度を気にするのであれば、その価値はあるかと思いますし、問題があれば issues で相談するのが良いと思います。

対して FOUC では他に譲る MathJax ですが、その開発の歴史は古く、数式の入力として TeX だけでなく MathMLサポートAsciiMath にも対応し、出力には Web フォントや SVG のみならず、音声読み上げへの対応など、入力と出力を柔軟に組み合わせられるモジュラー構成でしか対応できないユースケースもあることと思います。

あらためて、これらのオープンソース開発を担ってくれている方々に感謝です :shamrock:

用語について

参照した Web サイトによって、「文章中に表示される数式」と「単独の段落として表示される数式」を識別する用語にゆらぎが見られたので、本サイトでは、LaTeX project The LaTeX Project logo に掲載された、以下の文献中の用語「インライン数式(文章中の数式)」と「ディスプレイ数式(数式だけの行)」を使うことにしました。

参考文献

本記事中の評価を実施するに当たり、参考にさせていただいたサイト様や記事です。