デプロイ25分の内訳を計測して、削れる場所と削れない場所を分けた

開発mdx-playground

デプロイ25分の内訳を計測して、削れる場所と削れない場所を分けた

朝は前日の後始末のつもりだった。 昨日ブランチを切って作業した分が master に入っているか、それを確かめるだけの話だと思っていた。 終わってみると、この日はデプロイの時計を4回見ることになった。

昨日のブランチは本当に master に入っているのか

「いくつかブランチを作って作業した内容が昨日あると思うんですけど、そのあたりって全部マージ済みでしたっけ」と聞いた。 調べさせると、マージ済みと未マージがきれいに分かれない。 ブランチが master より単に古いだけの差分が混ざっていた。

そこでパッチ単位で判定し直させると、本体の作業はほぼ master に入っていることが確認できた。 取りこぼしは2件。 片方はブランチに残ったメモ1件、もう片方は別の worktree に未コミットで置き去りになっていた issue 11本だった。 どちらも衝突がないことを確かめさせてからマージし、push まで済ませた。

ブランチのマージ判定は「ブランチ名が master に取り込まれているか」ではなく「差分の中身が入っているか」で見る。 ここを取り違えると、実際には入っている作業を未マージと誤読する。

朝の決算データ取得で、Koyfin の auth_token cookie が見当たらず、cookie 認証だけで通していた。 issue には「httpOnly に変わった可能性」と書かせていた。

ここで引っかかった。 その作業のとき、いつも使っているブラウザとは別のブラウザを開いていたはずだ。 トークンが古くなったのではなく、単に見ているブラウザが違っただけではないか。 そう伝えて確認させた。

document.cookie は httpOnly を読めないので、読めないこと自体は「無い」の証拠にならない。 そこでネットワーク要求のヘッダ側から見させた。 いつものブラウザでは auth_tokendocument.cookie から普通に読めた。 新しいタブを開いても即座に読めた。 「cookie が消えた」という診断のほうが誤りだった。

issue を訂正させ、手順書の書き換えは不要になった。 ツールの仕様変更を疑う前に、自分がどこを見ていたかを疑う。

この状態でデプロイできるのか

その前後、コミットのたびに .git/index.lock に阻まれていた。 同じリポジトリで別の Claude Code セッションが git を連打しているせいだと思っていたが、6分間で90回チェックして一度も空かなかった。 残骸のロックだった。

気になったのはコミットのことではない。 「これは詰めてください。このままだと多分デプロイできないってことですか」と聞いた。

2つに分けて詰めさせた。 デプロイが止まるのかどうかと、ロックが残る原因と。

デプロイ経路のうち git の index に触るのは git status --porcelain の1箇所だけだった。 本体を止めずに試すため、独立した index を持つ worktree で残骸ロックを再現させて挙動を見た。 結果は明快で、残骸ロックがあってもデプロイは止まらない。 止まるのはコミットだけだった。

ついでに原因も割れた。 git status は実行中に index.lock を取る。 400回ポーリングさせたうち、119回と88回で実際に観測できた。 つまり git status の実行中にそのプロセスが外から殺されれば、ロックは必ず残る。

不安のまま止めずに済んだのは、その場で実測に落とせたからだった。

1回目のデプロイは、自分の変更と関係ない理由で落ちた

デプロイを起動した1回目は、7,729ルートのプリレンダーを終えたところで pnpm generate が exit 1 で落ちた。 原因は自分の変更ではなかった。 記事1本が /blog に出ておらず、その frontmatter に category と project_name が無い。 ビルドの最中に、別セッションが新しい記事を書いていた。

wrangler まで到達していないので、本番は前回のビルドのまま無傷だった。 競合の経緯を issue に残させ、content の書き込みが30分以上止まっているのを確認してから再実行した。

2回目は12:35に始まり、24分34秒で成功した。 ここで時間の中身を見にいった。

最初の仮説は「postgenerate が重い」だった。 既存の dist/ に対して読み取り専用のスクリプトだけ実測させると、合計およそ9秒。 仮説は外れた。

かわりにログを追わせると、12:54:13から12:58:26まで253秒、出力が1行も無い空白があった。 Nitro の外側でどこかが時間を食っている。 どこかは分からない。

走りっぱなしのプロセスを掃除する

自分でも measure-deploy.ps1 を回してみたが、Server Build に入って9秒で落ちた。 これは 2026-08-09 に判明済みの衝突で、dev サーバーが動いていると起きる。 dev を止めて、計測はこちらに任せることにした。 ちょうどマシン負荷が下がった状態での計測回になる。

そのついでに、走りっぱなしのプロセスも掃除させた。 15プロセスで5,156MB、空きは8.2GB。 デプロイ中のプロセスは絶対に落とさない条件で進めさせた。 まず明らかな残骸を1つ落として1,076MBを回収し、次に dev サーバー2つを止めさせた。 片方を止めたら、もう片方は既に消えていた。 子プロセスだった。

デプロイはそのまま走り続けた。

postgenerate の内訳を出せるようにする

dev を止めた回では数字に差が出た。 ただ、253秒の空白の正体は分からないままだった。 「内訳表示しましょうか。それでもう一回デプロイやってもらっていいですか」と頼んだ。

postgenerate の9本を計測付きのランナー経由に変えさせ、Nitro の仕上げ処理と各スクリプトの時間を分けて出るようにした。 measure-deploy.ps1 にも計測行を表示させる。

純粋関数として書かせてテストを付けたところ、最初はテストのほうが落ちた。 期待値の書き方が間違っていた((1.45).toFixed(1) は浮動小数の表現上 "1.4" になる)。 境界値を直させてテスト9件が通った。

計測付きでデプロイを回すと、233秒の空白の正体が割れた。 postgenerate が213.7秒だった。 しかも、その中の1本が大半を占めていた。

600回の全文走査を1回にする

犯人は、非公開記事が本番のダンプに漏れていないか検査するスクリプトだった。

遅い理由は gunzip だと思っていた。 違った。 展開後の35.6M文字の文字列に対して、非公開記事202本を最大3回ずつ、およそ600回にわたって全文走査していた。

600回の走査を1回のパスに置き換えさせた(Aho-Corasick)。 純粋関数として実装させ、String.includes との一致をランダム1000ケースで検証させて、テスト15件を通した。 そのうえで本番のキャッシュから実ダンプを取ってきて、旧実装と新実装の出力を突き合わせた。

277秒が40秒になった。 出力は完全に一致した(224行、警告15件、非公開203件)。

「速くなったはず」で終わらせず、同じ入力から同じ出力が出ることを実物で確かめてから差し替えさせた。

効くところと、まったく効かないところ

「結局これでやってもあんまり変わんないんですか、デプロイの時間って。Codex レビューのやつを潰したり、ログ削除とかやれば小さくなるって話じゃないですか。そこは関係ないんですか」と聞いた。

答えは2つに分かれた。 今回の変更で約2.5分縮む。 そして Codex レビューとログ削除はデプロイ時間に一切関係しない。 どちらもデプロイの処理そのものには含まれていない。

25.39分の内訳を並べさせると、大部分は Nitro のビルドとプリレンダーだった。 ここは記事とページの本数ぶんだけ積み上がる。 削るなら記事本数を減らすか、ページ生成の方式そのものを変えるしかない。

タグページのオンデマンド生成は見送った

そこで出てきたのが、タグページのオンデマンド生成という案だった。 「オンデマンド生成って何ですか。あとタグページってどこのことですか。ページとして開けますか」と聞いた。

実物を確認させると、タグページは609本あった。 今はビルド時にこの609本をすべて静的生成している。 オンデマンドにすれば、そのページに最初に来た人のリクエストで生成し、以降はキャッシュから返す。 ビルド時のコストは消えるが、そのぶん最初の1人が1〜2秒待つ。

「各ユーザーの時間を取るみたいなイメージってことですか」と聞いたら、そのとおりだという答えだった。 コストは消えるのではなく、払う相手が変わるだけだ。

見送ることにした。 609本のタグページに外部リンクや検索流入があるなら、それを失うリスクのほうがビルド時間の短縮より重い。 そして判断そのものを忘れると、また同じ案が出てくる。 残すと決めた理由をドキュメントに書かせた。

最後の計測は15分55秒

締めにもう一度デプロイを回した。 結果は15分55秒だった。 予想していた約20分を下回った。 公開記事4本が200、非公開は404で、本番も正常だった。

学びメモ

  • 速くする前に、どこで時間が使われているかを見られる状態を作る。最初に立てた「postgenerate が重い」という仮説は外れた
  • ログの空白は情報である。253秒どこにも出力が無いなら、そこに計測を挿す
  • 遅さの原因は圧縮展開ではなく、その後の総当たり走査だった。I/O を疑う前に計算量を数える
  • 実装の置き換えは、旧実装と新実装の出力が実データで一致することを確かめてから差し替える
  • ビルド時間の削減案は、削減量だけでなく「何を失うか」とセットで判断する。タグページのオンデマンド化は失うもののほうが大きいので見送った
  • 環境の仕様変更を疑う前に、自分がどのブラウザを見ていたかを疑う
#デプロイ#パフォーマンス計測 #Cloudflare Pages #Nuxt#プロセス管理