[🛠] 多言語ブログのビルド時間を21分から1分台まで縮める
✨ GPT-5.5の要約
多言語化のあと20分を超えていたJekyllビルドをprofile基準で追跡し、反復レンダリングと全サイトスキャンを取り除きながら1分50秒まで縮めた記録。
前回の多言語導入記事で、ブログに多言語運用構造を付けた。
最初は、これがかなり誇らしかった。
ko, en, ja, zh-Hans, es, pt-BR, fr, id。
記事もあり、メニューもあり、hreflangもあり、閲覧数も共有される。外から見ると、かなりそれらしい多言語ブログになっていた。
ところが、すぐに別の問題が飛び出した。
ビルドがあまりにも遅かった。
最初のproduction buildはこうだった。
done in 1311.791 seconds
21分52秒。
これは単純なブログから出てよい数字ではない。記事ひとつを直してビルドが20分以上かかるなら、そのうち記事を書く時間よりビルドを待つ時間のほうが長くなる。
最初は単純に「記事が8言語になったから遅くなったのだろう」と考えることもできた。
でも、それはあまりにも早すぎる結論だった。
ページ数が増えたのは事実だ。それでも20分台まで行くなら、何かがさらにある。だから今回は勘で直さず、jekyll build --profileを有効にした。
最初の犯人: カレンダーJSONをすべてのページに埋め込んでいた
まず_siteのサイズを見た。
_site: 1.2G
HTML total: 約840MB
静的ブログのHTMLが840MB。
あり得ない数字だった。
代表的な記事をひとつ開くと、すぐに理由が見えた。サイドバーのカレンダーが、すべての記事ページごとに、その言語の全記事一覧JSONをinlineで入れていた。
<script type="application/json" data-calendar-posts>
[
...
]
</script>
それだけではなかった。
カレンダーのfallback一覧を作るとき、日付ごとに全記事一覧をまた走査していた。条件に合わない記事もLiquidの空白として大量に出力されていた。画面に見える部分は小さいのに、HTMLの内部には巨大な空白と反復リストが隠れていた。
これは機能の問題ではなく、構造の問題だった。
カレンダーは、サーバーが完成形のHTMLとして毎ページ作る必要はなかった。すでにJSがカレンダーを動的に描ける。ならサーバーは現在月の基本骨格とデータファイルのパスだけを渡せばいい。
そこで言語別カレンダーデータを別JSONファイルに切り出した。
/assets/data/calendar-posts-ko.json
/assets/data/calendar-posts-en.json
/assets/data/calendar-posts-ja.json
/assets/data/calendar-posts-zh-Hans.json
/assets/data/calendar-posts-es.json
/assets/data/calendar-posts-pt-BR.json
/assets/data/calendar-posts-fr.json
/assets/data/calendar-posts-id.json
そしてページ側にはこの程度だけを残した。
data-calendar-posts-src="/assets/data/calendar-posts-en.json"
結果はすぐに縮んだ。
1311.791秒 -> 745.273秒
半分近く減ったが、まだ長かった。
そのとき分かった。
カレンダーは大きな犯人だったが、唯一の犯人ではなかった。
二番目の犯人: メニュー統計を8万回計算していた
次のprofileはもっと露骨だった。
_includes/sidebar-nav-stats.html 81120 calls 173.084s
_includes/masthead.html 2704 calls 319.860s
_includes/seo.html 2704 calls 119.985s
sitemap.xml 1 call 117.495s
ここでいちばん笑えたのはsidebar-nav-stats.htmlだった。
このincludeは、サイドバーのカテゴリ横に記事数と最新記事時刻を付ける小さな部品だ。
たとえばこういうものだ。
Daily Review (310) 1 days ago
Devlog (24) 2 days ago
ところがこの小さな部品が呼ばれるたびに、全記事一覧をもう一度ソートし、もう一度フィルタリングしていた。
言語別メニュー項目ごとに。
ページごとに。
デスクトップサイドバーとモバイルメニューでさらにもう一度。
結果として81,120回呼ばれていた。
この値は、本当はページごとに新しく計算する必要がない。同じ言語と同じメニューURLなら結果は同じだ。だからjekyll-include-cacheのinclude_cachedに変えた。
{% include_cached sidebar-nav-stats.html url=child.url lang=current_lang %}
すると呼び出し数はこう変わった。
81120 calls -> 210 calls
173秒 -> 0.4秒
これはほとんどバグを直したようなものだった。
三番目の犯人: 言語リンクを作るために全サイトをずっと走査していた
多言語切り替えを付けるとき、masthead, seo, sitemapにこんなロジックが入っていた。
この言語URLは実際に存在するのか?
意図は正しかった。
存在しない翻訳URLをhreflangや言語切り替えメニューに入れてはいけない。だから最初は、すべてのページとすべてのcollection文書を回って、URLの存在有無を確認していた。
問題は、これをすべてのページで繰り返していたことだ。
2704 pages * site.pages scan * translated collections scan
これは多言語サイトが大きくなるほど悪化し続ける構造だ。
そこで方式を変えた。
このブログの翻訳URLには、すでに規則がある。
/some/post/
/en/some/post/
/ja/some/post/
...
そして例外は別データで管理すればいい。
今回、翻訳がまだ保留になっていたセッション回顧記事は_data/i18n_pending.ymlに入れた。
entries:
- source_url: /devlog/github-pages-blog/github-pages-blog-english-version-lessons/
locales:
- en
- ja
- zh-Hans
- es
- pt-BR
- fr
- id
こうすると、通常の記事はprefix規則でつなぎ、保留記事は他言語ホームへfallbackする。全サイトスキャンはしない。
結果は大きかった。
masthead: 319.860秒 -> 9.882秒
seo: 119.985秒 -> 7.181秒
sitemap: 117.495秒 -> 4.633秒
そして最終ビルドはこう終わった。
done in 110.344 seconds
21分52秒から1分50秒。
この程度なら、まだ速いブログと言うには難しい。けれど少なくとも「ビルドが怖すぎて記事を書けない」状態からは抜け出した。
直しながら気をつけたこと
今回の最適化は、速度だけを見れば簡単そうに見える。
でも本当に気をつけるべきだったのは、機能が壊れる側だった。
特に多言語サイトでは、ビルドが速くなったと喜んでいるうちに、こんな問題が起こりうる。
言語切り替えリンクが404へ行く
hreflangが存在しないURLを指す
保留中の翻訳記事が検索エンジンにalternateとして露出する
モバイルでAbout/言語ボタンがまた見えなくなる
カレンダーが空のまま残る
だから最後には、ブラウザ確認と自動検査を一緒に回した。
確認したことはこうだった。
production build成功
英語旅行記事の8言語切り替えリンク正常
hreflang 8言語 + x-default正常
翻訳保留記事は他言語ホームへfallback
モバイルでAbout, 🇺🇸English, カレンダー表示正常
代表多言語URL 200
ソース記事2481件のレンダリング構造を全件確認
カレンダーJSONパース確認
i18n post coverage errors: 0
人間の目で2481件の記事をひとつずつ読んだわけではない。
ただ少なくとも「出力ファイルがあるか」、「記事構造がレンダリングされたか」、「言語リンクが壊れていないか」、「カレンダーデータが存在するか」は全件自動検査で確認した。
これが今回の作業の核心だった。
ビルド最適化は速度だけを減らす仕事ではなく、既存機能の契約をもう一度明示する仕事だ。
今回学んだこと
Jekyllは静的サイトジェネレーターなので、単純に見える。
でもLiquidの中で全サイトを何度も走査し始めると、静的サイトでも十分に重くなる。
特に多言語構造では、小さな非効率がそのまま掛け算で膨らむ。
ページ数 * 言語数 * メニュー数 * 全記事数
こういう掛け算が隠れていると、あとで突然ビルドが破裂する。
今回学んだことは単純だ。
第一に、ページごとに同じデータをinlineで繰り返さない。
第二に、同じ入力ならincludeをキャッシュする。
第三に、URLの存在有無を確認するために、すべてのページで全サイトをスキャンしない。
第四に、例外は勘で処理せず、データとして置く。
第五に、最適化後は機能契約を自動検査で確認する。
このブログは、単純な個人ブログから少しずつシステムになっている。
それは良くもあり、疲れることでもある。
でも少なくとも今回は、疲れる側に意味があった。
21分以上待っていたビルドを1分台まで引き下げたのは、体感としてかなり大きな転換点だった。
これで多言語ブログを育て続けるための、最低限の息継ぎはできた。
コメントする