torikatsu923.dev

サイト内検索

    【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 です。

    記事のmermaidのコードブロックから、ブラウザに届くファイルが作られるまでを示すフローチャート。執筆時に記事のMDXからSVGを生成する。ビルド時に記事のMDXを、imgでSVGを参照するHTMLに変換する。SVGとHTMLはブラウザへ配信される

    ダイアグラムを書いたら、手元で 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 つの問題が起きます。

    1. ダイアグラムのソースを変更したあとに pnpm run diagrams の実行を忘れると、古いダイアグラムのまま公開されてしまう
    2. 生成スクリプトが一部の 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 のダイアグラムを気軽に書けるようになりました。