ノートに戻る

公開 2026-01-05

私はどのようにして軽量なNextJSランタイムの構築に失敗したのか

l-you avatarまさにあなた

目標

本番運用中の NextJS アプリケーションを、重量級の Node.js standalone ランタイムから軽量なアーキテクチャへ移行しようと試みた。目的は、NextJS の output: 'export' を用いて静的アセットを生成し、それらを同じ Docker イメージにバンドルした軽量なカスタム Go サーバーで配信することだった。

そのアプリは、8 つのロケールをサポートする複雑な国際化対応サイトだった。主要要件はユニバーサルビルドを維持することだった。つまり、再ビルドなしで各ステージにデプロイできるよう、Docker イメージは環境変数に依存しない(アグノスティックである)必要があった。主目的は、重量級の NextJS ランタイムによる RAM 消費を取り除くこと。NodeJS/Bun のランタイムは、standalone モードで ~ 200-450MB の RAM、静的エクスポートで ~140MB を消費していた。

技術的実装

ユニバーサルビルドを実現するため、ビルド工程から環境変数を取り除いた。API エンドポイントを静的フロントエンドに焼き込む代わりに、/api/graphql のような静的パスを用いた。これらのリクエストの処理はバンドルした Go サーバーに任せ、実行時の環境変数で定義された適切なバックエンドサービスへプロキシとして転送させた。

画像最適化も同様に NextJS ランタイムから切り離した。専用の CDN パスへ書き換えるカスタム画像ローダーを実装し、静的エクスポートが画像処理ロジックに依存しないようにした。

課題:動的パス

スラッグでアクセスするブログ記事のような動的ルートの扱いに大きな障害があった。NextJS の Static Export では、ビルド時にすべてのパスを定義するため generateStaticParams が必要になる。あらゆるブログ記事を事前レンダリングする非効率を避けるため、動的ルートセグメントの代わりにクエリパラメータを使うようアーキテクチャを変更した。これにより、すべてのコンテンツ要求に対して汎用の静的 HTML ファイルを 1 つだけ配信し、クライアント側アプリがその後に個別のデータで埋める方式にできた。

致命傷:カノニカル URL

このプロジェクトは最終的に、SEO(検索エンジン最適化)、とりわけカノニカル URL メタデータの要件に関する解決不能な衝突のために断念した。

多くのメタデータは静的のままでも大きな影響はないが、カノニカルタグは正しく機能させるためにドメインを含む絶対 URL が必要だ。NextJS の app ディレクトリでは、メタデータは generateMetadata または静的な metadata エクスポートで定義する必要があり、静的エクスポートではどちらもビルド時にしか実行されない。Node.js サーバーなしに、これを実行時へ委ねる組み込みの仕組みはない。したがって、ユニバーサルな静的エクスポートではビルド時にドメイン名を知ることができない。

これを提案したアーキテクチャ内で正すには、Go サーバーが静的 HTML を解析し、実行時にカノニカルタグ文字列内のドメイン、またはドメイン様のプレースホルダー値を動的に置換する必要がある。この方法は、実質的に静的エクスポートに対して Go ベースのテンプレーティングを実装することを要求するため、却下した。そのような改変はエレガントではなく、サーバー送信の HTML を変更した結果、クライアント側の React アプリと不一致を起こすハイドレーション問題という重大なリスクを伴う。

TLDR

SEO の順位が不要なら、マルチ環境の Docker イメージでも軽量ランタイムは実現できる。そうでなければ、静的な .html.js 内のプレースホルダー ENV 変数をカスタムのテンプレートエンジンで置換する、といったかなりハック的な手段でしか実現できない。

BACKEND_URLAPI_ENDPOINT のような動的な環境変数やカスタムリライトルールが必要な NextJS アプリでは、軽量なカスタム Golang ベースのサーバーを使える。簡単で、実際に動く(~13 MB の RAM 使用量)。機能ごとに実装できるワークアラウンドも多い。

SEO が重要でないサイトであれば、静的エクスポートをホストできるサーバーはいくらでもある。たとえば私のプロジェクトのひとつでは、Rust-based static-web-server を使っている(~8 MB の RAM 使用量)。

Git 統計

今回の試みを数値で見ると次のとおり:

  • 65 files changed.
  • 173 insertions(+).
  • 1663 deletions(-).

もしあらゆる手段を尽くしてでも望む結果を得ようとするなら、更新にはおおよそ 挿入行が 4× 増える ことと、~100 ファイル にわたる変更が必要だった。

注目の添付

git changes screenshot of the project