発端: ビルドが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.vueとDocPage.vueからタグリンクの除去nuxt.config.ts側のgetTagRoutes参照の除去public/_redirectsとscripts/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.tsのconcurrency: 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に追記する
並列化に時間を溶かしかけたが、ドキュメントに「効かない理由」を残せたので、今日の時間は無駄ではなかったことにする。