2026.06.08 (月)

✨ 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-cacheinclude_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分台まで引き下げたのは、体感としてかなり大きな転換点だった。

これで多言語ブログを育て続けるための、最低限の息継ぎはできた。

コメントする