開発mdx-playground

発端: ビルドが20分で終わらない

朝、Cloudflare Pages へのデプロイログを眺めながら腕を組んだ。「あれ、また20分かかってる」。@nuxt/content v3 で生成している記事ページが 7,478 ルートに増え、prerender だけで 751 秒(約12.5分)。バンドル+ post-generate の処理を足すと、合計でほぼ20分が溶ける構造になっていた。

pnpm generate を回すたびに10分以上席を立つ運用は続けたくない。並列度を上げれば縮むのではないかと、ぼんやり手を伸ばしかけた。

ただ、手を動かす前に思い出した。「並列度の話、前にも一度やった気がする」。git ログを掘ると、過去に concurrency: 10 を入れて、その後外した形跡が残っていた。

そこで、まずは別の出血点から塞ぐことにした。

まず効いたのはタグページの削除

ビルドログを眺め直すと、prerender 件数のうち、結構な比率を /tags/<タグ名> のページが占めていた。タグページが何の役割を担っていたか、自分でも忘れていた。Claude Code に「タグページが何か実例を見せて」と頼んだら、pages/tags/[tag].vue の中身と、各記事の上部に並ぶタグリンクが出てきた。

中身を確認した瞬間、判断は早かった。「これ、別にいらない」。タグ別の記事一覧は、トップページのカレンダーや /blog の月別ビューで代替できる。タグごとに別ルートを切るほどの価値はなかった。

タグページを消す作業は、Claude Code に派遣して以下を一気にやらせた。

  • apps/web/app/pages/tags/[tag].vue の削除
  • apps/web/e2e/related-articles-and-tags.spec.ts の削除と、タグ無し版 related-articles.spec.ts の新設
  • ArticleTable.vueDocPage.vue からタグリンクの除去
  • nuxt.config.ts 側の getTagRoutes 参照の除去
  • public/_redirectsscripts/generate-redirects.mjs の整理

dev サーバで /blog のカレンダー表示、記事ページの上部、/tags/Vue が 404 になることをスクショで確認。問題なし。

デプロイログを送り返してもらったら、約20分 → 13分40秒(約32%削減)。タグ削除だけでこの効き目が出るとは、正直思っていなかった。

並列度8を入れてみたら逆に遅くなった

ここで本題の並列化に戻る。Nitro の prerender には nitro.prerender.concurrency という設定があり、デフォルトは 1。過去に 10 を入れて外した経緯がドキュメントに残っていたが、そこには「効かなかった」とまでは書かれていない。試す価値はある。

「8 で入れてみよう」と決めて、Claude Code に nuxt.config.ts を編集させた。同時に measure-deploy.ps1 の整形フィルタが concurrency: 8 (explicit) のログ行を握り潰していたので、表示パターンも追加させた。

再デプロイ。結果のログを眺めた瞬間、声が漏れた。「あ、これ、遅くなってる」。

prerender 時間は concurrency: 1 のとき と比べて、concurrency: 8 にしたほうがむしろ長いという結果。誤差ではなく、はっきり遅い。スレッドの取り合いで context switching が増えただけ、という挙動に見えた。

Codex に聞こうとしたが Web で答えが出てしまった

ここで、判断を一段深くする。「並列化が効かないのは設定ミスか、それとも Nitro 側の構造の問題か」を切り分けたい。

ユーザーに「Codex にも一応真相を聞いてみてほしい」と頼まれたので、まず Codex CLI を叩いた。ところがモデル名のエラーで蹴られた。別モデルで再試行する前に、並行で Web 検索を回したら、決定的な答えが出てきた。

Nitro の GitHub Issue #1447 で議論されている通り、Nitro の prerender は worker_threads を使っていないため、concurrency を上げても並列化が効かない。同一プロセス内の非同期処理が増えるだけで、CPU バウンドな Vue/Nuxt のレンダリング処理は順番待ちになる。

つまり、過去に concurrency: 10 を外したのは正解だった。Codex に追加で聞く必要もない、と判断して撤回。

A プランで撤回、ドキュメントに残す

「結局意味なかったってことですね。じゃあドキュメントに残して戻してくれてるってことですね」と、ユーザーから一言。撤回作業に入った。

撤回内容:

  • nuxt.config.tsconcurrency: 8 行を削除
  • memo/2026-06-16/build-time-and-tags-removal.md に「並列化を試した結果と、なぜ効かなかったか」を追記
  • Nitro Issue #1447 への参照を残し、今後同じ罠を踏まないようにする

このドキュメントは、半年後の自分が「ビルド遅いな、並列化試すか」と思ったときに最初に当たる地雷探知機の役割を担う。試して、ダメで、戻した。ここまでを丸ごと残すから、次の自分は2度目の試行錯誤に時間を溶かさずに済む。

学び

今日の試行錯誤で、頭の中に固定された判断軸がいくつかある。

1. ビルド時間の最適化は、まず「ルート数を減らす」が一番効く

Nitro の prerender が CPU バウンドかつ並列化が効かない以上、勝ち筋は「不要なルートを削る」しかない。タグページのように、需要が薄い割にルート数を膨張させているページを真っ先に疑う。

2. 過去の試行錯誤の痕跡は git に残っているので、まず履歴を当たる

「前にも一度やった気がする」という違和感を拾ったら、即 git log と git diff で過去のコミットを掘る。今回も「過去に concurrency: 10 を入れて外した」事実が見つかり、最初から仮説が立てやすかった。

3. ダメだった試行も「ダメだった」とドキュメントに残す

成功した変更だけドキュメントに残すと、半年後の自分が同じ罠に再突入する。「試したけど効かなかった」「Nitro Issue #1447 が根本原因」までセットで書く。

4. Codex も Web 検索も、両方手駒として並列で走らせる

今回は Codex がモデル名で蹴られたが、その間に Web 検索で答えが出てしまった。AI への質問と Web 検索は、片方が詰まったときの保険として並列で回すのが結局速い。

明日以降の宿題

  • ビルド時間をさらに削るなら、記事の生成スキーマや useAsyncData の payload サイズを点検する
  • pages/[...slug].vue のレンダリングで重い処理がないか、prerender 1ルートあたりの所要時間を計測する
  • 13分40秒からさらに削るアイデアが出たら、また memo/2026-06-16/build-time-and-tags-removal.md に追記する

並列化に時間を溶かしかけたが、ドキュメントに「効かない理由」を残せたので、今日の時間は無駄ではなかったことにする。