【Astro】MermaidのダイアグラムをJSの配信なしで表示するため、執筆時にSVGへ事前描画する仕組みを作った
Astro で作ったこのブログでは、記事に Mermaid のコードブロックを書くと、執筆時に事前描画した SVG の画像として表示されます。Mermaid の JS を配信せずに、Mermaid のダイアグラムを記事中に表示できます。ダイアグラムを表示する 4 つの方法の比較と、事前描画した SVG を記事のソースどおりに保つ仕組みを紹介します。
はじめに
こんにちは、とりかつ(@torikatsu923)です。
このブログは Astro で作っており、Lighthouse の 4 カテゴリすべてで 100 を取ることを目標にしています。最近、記事の内容をよりわかりやすくするため、Mermaid のダイアグラムを記事中に配置したいと考えました。しかし、Mermaid をブラウザで描画するには Mermaid の JS を配信する必要があり、ページの転送量が増えてしまいます。
そこで、Mermaid のダイアグラムは執筆時に手元で SVG へ描画しておき、ビルド時に記事の ```mermaid のコードブロックをその SVG の画像へ差し替えるようにしました。差し替えることで、Mermaid の JS を配信することなく、Mermaid のダイアグラムを記事中で表示できました。
この記事では、Mermaid のダイアグラムを表示する 4 つの方法の比較と事前描画の仕組みを説明したうえで、事前描画した SVG を記事のソースどおりに保つための対策を中心に紹介します。
Mermaid のダイアグラムを表示する 4 つの方法の比較
Mermaid のダイアグラムをブログに表示する方法は、Mermaid をいつ実行するかと、ブラウザで実行する場合の JS の読み込み方で、4 つに分かれます。
| 方法 | Mermaid の JS を配信しない | ビルド時にヘッドレスブラウザが不要 | 執筆時にやること |
|---|---|---|---|
| ブラウザで描画 | × | ○ | コードブロックを書く |
動的 import() で描画 |
× | ○ | コードブロックを書く |
| ビルド時に描画 | ○ | × | コードブロックを書く |
| 執筆時に事前描画(採用) | ○ | ○ | コードブロックを書き、SVG を生成してコミットする |
ブラウザで描画する方法では、Mermaid の JS を配信します。mermaid 11.17.2 の dist/mermaid.min.js は、gzip -9 で圧縮した後のサイズが約 980 KB あります。ダイアグラムのある記事に限って動的 import() で読み込む方法も検討しましたが、ダイアグラムがファーストビューにあれば、初期ロードで読み込まなければなりません。
ビルド時に描画する方法(rehype-mermaidなど)では、JS は配信されません。ただし、Mermaid はノードの大きさをブラウザでテキストを実測して決めるので、ビルド環境にヘッドレスブラウザの Chromium が必要です。このブログをビルドしている GitHub Actions と Cloudflare Workers Builds の両方に Chromium をインストールすると、ビルド時間と依存が増えます。さらに、ビルド環境ごとにフォントが違えば、同じソースからでもダイアグラムの寸法が変わります。そのため、ビルド環境には Chromium をインストールしたくありませんでした。
Mermaid の JS を配信せず、ビルド環境にも Chromium をインストールせずに済むので、今回は執筆時に事前描画する方法を採用しました。
執筆時に生成した SVG にコードブロックを差し替える仕組み
記事には、Mermaid のダイアグラムを ```mermaid のコードブロックで書きます。コードブロックの開始行には、ダイアグラムの内容を説明する alt を付けます。
```mermaid alt="記事のmermaidのコードブロックから、ブラウザに届くファイルが作られるまでを示すフローチャート。…"
flowchart LR
...
```
以下は、記事のコードブロックから、ブラウザに届くファイルが作られるまでを示したフローチャートです。矢印は、どのファイルからどのファイルが作られ、どこへ渡るかを表します。このフローチャートも、この仕組みで描画した SVG です。
ダイアグラムを書いたら、手元で pnpm run diagrams を実行します。pnpm run diagrams は、すべての記事のうち、まだ SVG を生成していない mermaid のコードブロックを対象に、mermaid-cli で SVG を生成するコマンドです。SVG は src/assets/diagrams/ に生成されるので、記事と一緒にコミットします。
ビルド時には、Markdown を HTML に変換する前の段階で、mermaid のコードブロックを生成済みの SVG の画像に置き換えます。コードのハイライトより前に置き換わるので、ダイアグラムのソースがコードとして表示されることはありません。HTML が重くならないよう、SVG は埋め込まず <img> で読み込みます。
事前描画で起きる 2 つの問題と対策
事前描画では、ダイアグラムのソース(記事のコードブロック)と、そこから生成した SVG が別々のファイルになります。そのため、何も対策しなければ 2 つの問題が起きます。
- ダイアグラムのソースを変更したあとに
pnpm run diagramsの実行を忘れると、古いダイアグラムのまま公開されてしまう - 生成スクリプトが一部の mermaid のコードブロックを見落とすと、その SVG が作られず、ビルド時のエラーが解消しない
SVG の作り直し忘れの検出
仕組みの節で書いたとおり、ビルド時には mermaid のコードブロックを対応する SVG の画像に置き換えています。そのため、ダイアグラムを変更したあとに SVG を再生成していないと、古い SVG の画像のままページが公開されてしまいます。つまり、pnpm run diagrams の実行漏れによって、画像を更新し損ねるおそれがあります。
コードブロックと SVG の紐付けは、ファイル名で行っています。そこで、このファイル名をダイアグラムのソースから生成したハッシュにすれば、ダイアグラムを変更するたびにファイル名が変わり、変更前の SVG を使ってしまうことを防げると考えました。
ハッシュの生成に使用しているのは、ダイアグラムのソースを含め、リポジトリの中で SVG の出力に影響する 4 つです。
- 生成に使用するパッケージの版(mermaid-cli・mermaid・puppeteer)
- mermaid-cli に渡す引数(背景色など)
- 全ダイアグラムで共有する Mermaid の設定ファイルの中身
- ダイアグラムのソース
(別の環境でビルドしても同じファイル名で SVG を見つけられるように、手元の OS とフォントはハッシュの生成に使用していません。)
これにより、ダイアグラムを 1 文字でも変更すると、ファイル名が変わります。ビルド時には、mermaid のコードブロックに対応する SVG が存在するかをファイル名で確認しており、見つからなければ pnpm run diagrams の実行を促すエラーで停止します。
生成スクリプトが mermaid のコードブロックを見落とす問題への対策
pnpm run diagrams の生成スクリプトは、記事から mermaid のコードブロックを探し、それぞれの SVG を生成します。このとき、生成スクリプトがコードブロックを独自の方法で探すと、MDX の書き方によっては一部を見落とします。
たとえば MDX では、字下げした行がコードブロックにならないので、4 文字字下げした ```mermaid のコードブロックも、ビルド時には差し替えの対象になります。生成スクリプトがこれを見落とすと、その SVG が作られないので、pnpm run diagrams を実行してもビルド時のエラーが解消しません。
そこで、ビルド時のパーサと判定関数を、生成スクリプトでもそのまま使用するようにしました。
パーサは、Astro が MDX の変換に使用する Sätteri です。生成スクリプトは版と設定をビルド時とそろえて記事を変換し、差し替えの処理が受け取るものと同じ mermaid のコードブロックを取得します。
取得したコードブロックは、差し替えの処理と共通の関数で、ダイアグラムとして書き方が正しいかを判定します。判定に通ったコードブロックからは、ハッシュを生成します。SVG の一部だけを更新した中途半端な状態を作らないために、判定でエラーになるものが 1 つでもあると、生成スクリプトは SVG の生成も古い SVG の削除もせずに停止します。
事前描画を導入した後の Lighthouse のスコア
事前描画を導入した後に、Lighthouse CI(モバイル基準)で計測しました。CI の計測対象の 5 ページを 3 回ずつ計測した全 15 回で、4 カテゴリすべてが 100 になり、TBT は 0 ms でした。CI の計測対象に入っていない、ダイアグラムのある記事(/posts/claude-tmux-sidebar/)も、Lighthouse で 3 回計測して 4 カテゴリすべてが 100 でした。ビルドの出力(dist/)にも、Mermaid の JS は含まれていません。
事前描画で増えた執筆の手間を許容した理由
一方で、事前描画にしたことで、執筆の手間が 2 つ増えました。
- mermaid-cli が使用する Chrome が手元で必要になる
- ダイアグラムを書いたり変更したりするたびに、
pnpm run diagramsを実行して SVG をコミットする必要がある
前の節の結果と比べれば、どちらの手間もダイアグラムを書く・変更する執筆時に限られるので、許容しました。
おわりに
この記事では、Mermaid のダイアグラムを執筆時に SVG へ事前描画して画像に差し替える仕組みと、古い SVG のまま公開されるのをビルド時に検出する仕組みを紹介しました。これで、公開ページの JS を増やさずに、記事をわかりやすくする Mermaid のダイアグラムを気軽に書けるようになりました。