Herokuを止められるか、nuxt generate を4回回して確かめた — 旧Nuxt 2アプリ静的化の実測
計画書に「静的化してHerokuを止める」と1行だけ書いてあった。読み返していて手が止まった。
前日の続きから
昨日の棚卸しで論点を5つ残したまま終わっていたので、まず仕掛かりを洗い出させた。出てきたのは2件。片方はほぼ終わっていたので、もう片方のサイト整理へ進み、計画書をChromeの前面タブに開かせた。
論点は判断の前提を読みながら答えたい。ターミナルの選択肢では前提が見えないので、計画書のHTMLに3択のボタンを埋め込む形にしてもらった。
読み進めていて、課題の一覧から学習アプリが抜けているのに気づいた。Herokuで動いている旧世代のアプリで、昨日の調査では別枠扱いになっていた。指摘して、課題に加えさせた。ついでにアフター像を図にしてほしいと頼んだら「もう描いてあります」と返ってきて、タブの奥に埋もれていた2枚が出てきた。ビフォーとアフターを並べたSVGで、カードの高さが揃っていない箇所だけ直させた。
決裁フォームの方は少しもたついた。サーバーは動いてHTTP 200を返しているのに、こちら側のバックグラウンド追跡が2回続けて切れた。その状態だと、回答を送ってもこちらが気づけない。3回目は立て直しをやめて、回答ファイルが作られた瞬間を検知する監視に切り替えさせた。11時09分に5問すべての回答が届いた。
学習アプリについて選んだのは「静的化してHerokuを止め、移譲は順次進める」だった。
一行の記述に引っかかった
回答をもとにフェーズ2の実行計画を作らせた。そのステップ2が「静的化してHerokuを止める」である。
読んでいて、順序がおかしい気がした。この計画では、Vue 2で書かれた画面をVue 3へ移す作業が先にあるように読める。だが静的化とは生成済みのHTMLとJSを配信することだ。だとすれば、ビルドさえ通れば書き換えを待つ理由はない。Herokuを解約して、生成物をCloudflareに置いて、DNSを向け直す。今日でも終わる話ではないのか。
そう聞いた。合っているなら、月々の支払いが1つ今日消える。
ただし解約は戻せない。前のセッションの調査結果を鵜呑みにさせず、リポジトリを自分で読んで裏を取るところから始めさせた。
サーバー側は33行しかなかった
コードを読ませた結果は、拍子抜けするほど薄かった。
- Expressのエントリは33行。Nuxtのレンダラを
app.useするだけで、独自ルートもDB接続も認証処理もない process.envの参照は1箇所(NODE_ENV)のみ。他はすべてコメントアウト済み- ページは799ファイル、動的ルートは1本だけ
- 記事サイトからの最多参照先である仕訳問題のページ(68箇所)は
asyncDataを持たない。データをJSONのimportでバンドルし、クエリはクライアント側で読んでいる
手当てが要るものは3つに絞れた。外部APIを叩く動的ルート1本、ランタイムプロキシを使う1ページ、HTTPS強制のミドルウェア。どれも記事サイトからの導線には乗っていない。
計画書には「関門はNode 18でのビルド」と書いてあった。旧世代のwebpackはNode 18より新しい環境でOpenSSL絡みのエラーを吐く。手元にはNode 22しかない。
関門はOpenSSLではなくメモリだった
--openssl-legacy-provider を付けて走らせた。無害で可逆なので、確認を挟まずそのまま実行させた。
8分後、EXIT=134で落ちた。
ログを見ると、webpackのコンパイルはClient / Serverとも通っていた。OpenSSLは何も起こしていない。落ちたのは799ページのレンダリング段階で、ヒープ4GBを使い切っていた。計画書に書いた関門の想定が、そもそも外れていたことになる。
搭載32GBのうち12GBを割り当てて2回目。
NODE_OPTIONS="--openssl-legacy-provider --max_old_space_size=12288" \
./node_modules/.bin/nuxt generate
メモリ不足は再発しなかった。通知には「exit code 0で完了」と出た。
その報告は取り消された。0はラッパーコマンド全体の値で、nuxt generate 自体はEXIT=1で終わっていたという。794ルートは生成できていて、失敗が6件あった。誤報が自己申告で訂正されたのはよかったが、通知の数字を信じてはいけないという事実だけが残った。
失敗6件は、静的化のせいではなかった
内訳を見て、少し笑ってしまった。
1件は想定どおりの動的ルートだった。残り5件はすべて、存在しないページを指しているサイト内リンクである。加えて、生成を止めていた致命的なエラーの正体は <NuxtLink> の to に残った http://localhost:3000/... の1行だった。クローラーがそれを辿り、Windowsのパスとして無効な : を含むディレクトリを作ろうとして落ちていた。
どれも静的化で生まれた問題ではない。SSRなら404を返して終わるので誰も気づかなかっただけで、本番には開発用のURLへ飛ぶリンクが出ていた。静的生成がそれをエラーとして表に出した。
「一旦リンクを外していいので、3つまとめて完走させて」と指示した。
返ってきたのは、指示と少し違う結果だった。
消す前に、行き先が実在していた
調べたところ、リンク切れ5パターンのうち4つは正しいページが実在していたという。06_ というファイル名のプレフィックスが落ちている、cockpit が ccockpit になっている、単数形と複数形が食い違っている。要するにタイポである。
消せば本来つながるはずの導線まで失う。だから実在が確認できた7箇所は正しいパスに直し、行き先が自明でない1パターン6箇所だけリンクを外した。テキストは残してある。指示と違う判断をしたという申告が先にあったので、そのまま通した。
外した1パターンは、統合ページが3つに分割された跡のように見える。どこへ向けるべきか、いまの私にも決められない。
外部APIに依存する動的ルートは、生成の対象から外した。
generate: { exclude: [/^\/questions$/] },
3回目のビルドはEXIT=0で完走した。失敗は1件だけ残り、それも同じプレフィックス欠落だった。2回目は途中で止まっていたので、その行まで到達していなかっただけである。同じ要領で直して4回目を回した。
| 回 | 終了コード | 生成ルート | 失敗ルート |
|---|---|---|---|
| 1回目 | 134(メモリ不足) | — | — |
| 2回目 | 1(致命エラー) | 794 | 6 |
| 3回目 | 0 | 794 | 1 |
| 4回目 | 0 | 794 | 0 |
402箇所を照合したら、25件の欠けは集計バグだった
生成できたことと、リンクが切れないことは別の話だ。記事サイトのソース全体から学習アプリへの参照を機械的に抽出させ、生成物と1件ずつ突き合わせさせた。
最初の集計では25件が「生成物に無い」と出た。抽出の正規表現がMarkdownのリンク記法の ]( を拾って、実在しないパスを作っていたのが原因だった。区切り文字を足して再集計すると、ユニーク39URL・延べ402箇所、欠けは0件になった。
数字が悪い方に振れたときは、まず自分の集計を疑う。今回はそれで正解だった。
生成物をローカルで配信して実物も見た。最多参照先の仕訳問題を ?chapter=1§ion=1 付きで開くと、18問が読み込まれ、問題文も借方貸方の選択肢もボタンも描画された。コンソールのエラーと警告は0件だった。ログイン画面も認証フォームまで出た。コードから「クエリはクライアント読み」と判断した内容がそのまま実挙動と一致している。
Cloudflare Pagesの制限も測らせた。総ファイル数4,610に対して上限は20,000、25MBを超える単一ファイルは0件だった。総容量750MBのうち375MBがレッスン、179MBが画像である。画像は重いが、制限には当たらない。
それでも、今日は止めない
技術的にできることは実測で確定した。ここで解約に走りたくなるが、まだ静的化した生成物をデプロイしていない。
止める順番はこうなる。まず静的版をCloudflareに載せてDNSを向ける。そのうえでHeroku側へのアクセスが消えたことを確認する。誰も来ていないと分かってから停止する。この確認を飛ばすと、まだ生きている導線を自分で切ることになる。
検証記録と実行計画をドキュメントに残して、学習ゲートをスキップしてコミットした。停止判断は明日の積み残しとして登録してある。
残ったもの
前提を1つ壊した。計画書に書いた「関門はNodeのバージョン」は外れで、実際の関門はメモリだった。手元のマシンで12GBを渡せば通るものを、旧バージョンのNodeを用意するところから始めようとしていた。
そしてビルドを4回回して見えたのは、静的化の可否ではなく、SSRが隠していた不備の量だった。本番に出ていたlocalhostへのリンクが1本、タイポで404に落ちるリンクが5パターン。SSRは404を返して黙るが、静的生成はビルドを止めてまで教えてくる。うるさいが、正しい。
止めるかどうかは明日決める。決め手はビルドの成否ではなく、アクセスログだ。