Read OSS

ビルドパイプライン:webpack の設定、コード生成、そして3つのコンパイラアーキテクチャ

上級

前提知識

  • 記事1〜2:アーキテクチャ概要とサーバーの起動シーケンス
  • webpack の基本概念(ローダー、プラグイン、コンパイルフェーズ、モジュールグラフ)
  • モジュール解決とコード分割の仕組み
  • React Server Components のサーバー/クライアント境界の理解

ビルドパイプライン:webpack の設定、コード生成、そして3つのコンパイラアーキテクチャ

next build を実行すると、JavaScript エコシステムの中でも屈指の複雑なビルドパイプラインが動き出します。ファイルシステムからルートを探索し、フレームワークの基盤とユーザーのコンポーネントを結びつけるエントリーポイントコードを生成し、3つの独立した webpack コンパイル(client・server・edge)を実行し、十数種類のマニフェストを出力します。さらに、静的なページはすべてプレレンダリングされます。この記事では、そのパイプライン全体を追っていきます。

ビルドのオーケストレーション:build/index.ts

ビルドのエントリーポイントは build/index.ts です。約4,330行に及ぶこのファイルが、ビルドプロセス全体の流れを制御しています。

flowchart TD
    Start["next build"] --> Config["Load & Validate Config"]
    Config --> Env["Load .env files"]
    Env --> FindPages["Find pages/ and app/ directories"]
    FindPages --> CustomRoutes["Load custom routes\n(rewrites, redirects, headers)"]
    CustomRoutes --> Entries["Collect Entrypoints\n(entries.ts)"]
    Entries --> Compile["Run Webpack Compilation\n(3 compilers)"]
    Compile --> Manifests["Generate Manifests\n(pages, routes, prerender, etc.)"]
    Manifests --> StaticGen["Static Generation\n(prerender pages)"]
    StaticGen --> Export["Export static files"]
    Export --> Output["Write build output\n(.next/)"]

ビルドはまず設定の読み込みから始まります(記事1で紹介した loadConfig() と同じものです)。次に pages/app/ ディレクトリを探索し、next.config.js の rewrites・redirects・headers からカスタムルートを読み込み、ファイルシステム上のすべてのエントリーポイントを収集してから、webpack のコンパイルを開始します。

コンパイル完了後は、静的生成のフェーズに移ります。プレレンダリング可能なすべてのページを描画し、HTML と RSC データを生成します。ISR ページの初回キャッシュエントリーが作成されるのもこのタイミングであり、PPR ページの静的シェルもここで生成されます。

エントリーポイントの収集とコード生成

entries.ts モジュールは、ファイルシステムからルートを探索して webpack のエントリーポイントへとマッピングします。各ルートに対してエントリーポイントのコードを生成する仕組みが ビルドテンプレート です。これはランタイムのリフレクションを使わないコード生成の仕組みです。

flowchart LR
    FS["Filesystem\n(app/dashboard/page.tsx)"] --> Entries["entries.ts\n(route discovery)"]
    Entries --> Template["Build Template\n(app-page.ts)"]
    Template --> EntryCode["Generated Entry\n(imports user code +\nframework wrappers)"]
    EntryCode --> Webpack["Webpack Entry"]

ビルドテンプレートは build/templates/ に置かれています。

テンプレート 役割
app-page.ts App Router のページコンポーネントを AppPageRouteModule でラップする
app-route.ts App Router のルートハンドラーを AppRouteRouteModule でラップする
pages.ts Pages Router のページを SSR/SSG 基盤でラップする
pages-api.ts Pages Router の API ルートをラップする
middleware.ts middleware を edge ランタイムアダプターでラップする
edge-ssr-app.ts App Router の Edge ランタイム SSR 用

中でも app-page.ts テンプレートは特に興味深い構造をしています。記事3で紹介した AppPageRouteModule クラス、ユーザーの loaderTree、そしてベンダー化された React インスタンスをインポートし、それらを組み合わせます。また、with { 'turbopack-transition': 'next-ssr' } のようなインポートアトリビュートを使い、各モジュールが対象の実行環境に合った正しいバンドラー設定でロードされるようにしています。

このコード生成アプローチは重要な設計上の判断です。リクエスト時に require(userPagePath) でランタイムリフレクションを行う方法ではなく、ビルド時に静的なインポートを生成します。これにより、デッドコード削除やツリーシェイキングが有効になり、バンドラーが各エントリーポイントの依存関係を正確に解析できるようになります。

ヒント: ビルドの問題をデバッグするときは、.next/server/app/[route]/page.js にある生成済みエントリーポイントコードを確認しましょう。テンプレートがページコンポーネントをフレームワークとどう結びつけたかが、そのまま確認できます。

webpack 設定ファクトリー

webpack-config.ts ファクトリー(約2,930行)は、3つの異なる webpack 設定を生成します。記事1で紹介した3つのコンパイラターゲットに対応するものです。

flowchart TD
    Factory["webpack-config.ts\ngetBaseWebpackConfig()"] --> Client["Client Config"]
    Factory --> Server["Server Config"]
    Factory --> Edge["Edge Server Config"]

    Client --> ClientBundle["Browser JavaScript\n- React client components\n- Client-side router\n- CSS/assets"]

    Server --> ServerBundle["Node.js Bundle\n- RSC rendering\n- API routes\n- SSR"]

    Edge --> EdgeBundle["Edge Bundle\n- Middleware\n- Edge routes\n- No Node.js APIs"]

    subgraph "Key Differences"
        ClientDiff["Client:\n- target: 'web'\n- externals: none\n- splits code by page"]
        ServerDiff["Server:\n- target: 'node'\n- externals: node_modules\n- bundled React"]
        EdgeDiff["Edge:\n- target: 'webworker'\n- polyfilled APIs\n- size-constrained"]
    end

3つの設定は共通の基盤を持ちながら、以下の点で大きく異なります。

  • Target'web'(client)、'node'(server)、'webworker'(edge)。
  • Externals:server コンパイラは node_modules を外部化します(ランタイムで利用可能なため)。一方、client コンパイラはすべてをバンドルします。edge コンパイラは選択的にバンドルし、利用できない Node.js API はポリフィルで補います。
  • モジュール解決:server コンパイラは RSC モジュールに react-server 条件を使い、client コンパイラは標準の browser 条件を使います。'use client' 境界が機能する仕組みはここにあります。同じパッケージでも、コンパイラによって異なるコードへと解決されるのです。
  • webpack レイヤー:server コンパイラはレイヤー(WEBPACK_LAYERS)を使って RSC コードと SSR コードを分離し、サーバー専用のインポートがクライアントバンドルに漏れないようにします。

また、最適化の設定も含まれています。client 向けのコード分割、server 向けのチャンク戦略、CSS の抽出などです。開発時には ReactRefreshWebpackPlugin によって React Fast Refresh が有効になります。

重要な webpack プラグイン:Flight Manifest と Client Entry

React Server Components をサーバー/クライアント境界をまたいで機能させるために欠かせないプラグインが2つあります。

FlightManifestPlugin

flight-manifest-plugin.tsClient Reference Manifest を生成します。これは、サーバーがクライアントコンポーネントをどのように参照するかを示すマッピングです。Server Component がクライアントコンポーネントを描画する際、そのコンポーネントのコードは直接描画されず、参照として出力されます。この Manifest が、その参照と実際のクライアントサイドのチャンクファイルを対応付けます。

flowchart LR
    ServerComp["Server Component\n(renders <ClientButton />)"] --> Ref["Client Reference\n{id: 'Button', chunks: [...]}"]
    Ref --> Manifest["Client Reference Manifest\n(flight-manifest.json)"]
    Manifest --> ClientChunk["Client Chunk\n(Button.js)"]

    subgraph "Server Bundle"
        ServerComp
        Ref
    end

    subgraph "Client Bundle"
        ClientChunk
    end

    Manifest -.->|"Maps references\nto chunks"| ServerComp
    Manifest -.->|"Tells browser\nwhat to load"| ClientChunk

FlightClientEntryPlugin

flight-client-entry-plugin.ts はモジュールグラフを走査して 'use client' 境界を探し出します。'use client' ディレクティブを持つモジュールを見つけると、そのモジュールとその依存関係に対してクライアントサイドのエントリーポイントを生成します。「クライアントコンポーネント」という抽象概念がバンドラーレベルでどう実現されているかがここにあります。このプラグインが 'use client' 境界でモジュールグラフを自動的に分割しているのです。

また、Server Actions('use server')の処理も担います。クライアントからサーバー関数を呼び出す方法を示す Server Reference Manifest を生成することで、その逆方向のマッピングを提供します。

この2つのプラグインは、React Server Components(ランタイムの概念)と webpack(ビルド時のツール)をつなぐ橋梁です。モジュールグラフを解析してサーバー/クライアント境界を把握し、両方のランタイムがその境界をまたいでコードを参照するためのマニフェストを生成します。

App ローダー:ファイルシステムからモジュールグラフへ

next-app-loader は、app/ ディレクトリの規約をレンダリングエンジンが消費する loaderTree データ構造へと変換する webpack ローダーです。たとえば次のようなディレクトリ構成があるとします。

app/
  layout.tsx
  page.tsx
  dashboard/
    layout.tsx
    page.tsx
    loading.tsx
    error.tsx

このローダーは以下のようなツリーを生成します。

graph TD
    Root["['', { layout: './app/layout.tsx', children: ... }]"] --> Dashboard["['dashboard', { layout: './app/dashboard/layout.tsx',\nloading: './app/dashboard/loading.tsx',\nerror: './app/dashboard/error.tsx',\nchildren: ... }]"]
    Dashboard --> Page["['page', { page: './app/dashboard/page.tsx' }]"]

ローダーツリーの各ノードは [segment, modules, children] というタプル形式です。modules オブジェクトには、layout.tsxpage.tsxloading.tsxerror.tsxtemplate.tsxnot-found.tsx といった規約ファイルへの参照が含まれています。この構造は webpack のモジュールグラフにシリアライズされ、後から create-component-tree.tsx(記事3参照)が React 要素ツリーを構築する際に利用されます。

また、このローダーはパラレルルート(@ で始まるディレクトリ名)、ルートグループ(括弧で囲まれたディレクトリ名)、インターセプティングルートにも対応しています。いずれもファイルシステムの規約を解釈し、適切なツリー構造を生成することで実現されています。

ヒント: レイアウトやエラー境界が想定どおりに動かないとき、ローダーツリーを確認してみましょう。生成されたページモジュールの中で loaderTree を検索すれば見つかります。このツリー構造が、コンポーネントのネスト方法に直接対応しています。

代替バンドラー:Turbopack と Rspack

ここまで説明してきた webpack ベースのパイプラインはいわば「リファレンス実装」です。Turbopack と Rspack は、コンパイルのステップで代替として差し込む形で機能します。

flowchart TD
    Build["build/index.ts"] --> BundlerCheck{"Which bundler?"}

    BundlerCheck -->|Webpack| WebpackConfig["webpack-config.ts\n(3 compilers)"]
    BundlerCheck -->|Turbopack| TurbopackBuild["turbopack-build/\n(Rust compilation)"]
    BundlerCheck -->|Rspack| RspackBuild["next-rspack/\n(Rspack compilation)"]

    WebpackConfig --> Manifests["Shared Manifest Format"]
    TurbopackBuild --> Manifests
    RspackBuild --> Manifests

    Manifests --> StaticGen["Static Generation\n(shared across bundlers)"]
    Manifests --> ServerRuntime["Server Runtime\n(shared across bundlers)"]

重要なのは、3つすべてのバンドラーが 同一のマニフェスト形式 を出力するという点です。サーバーランタイムはどのバンドラーがマニフェストを生成したかを気にしません。build-manifest.jsonpages-manifest.jsonclient-reference-manifest.json などを読み込むだけで、ビルドツールには依存しません。

Turbopack のビルドパスは build/turbopack-build/ にあり、crates/next-core の Rust クレートに処理を委譲します。エントリーポイントの探索とコード生成テンプレートは webpack と同じものを使いますが、実際のバンドル処理は Turbopack の Rust エンジンが担います。インクリメンタルな計算と並列処理のおかげで、大幅に高速です。

packages/next-rspack にある Rspack は webpack と API 互換です。webpack-config.ts の設定の多くをそのまま再利用できます(Rspack は webpack のドロップイン代替として設計されています)が、実際のコンパイルは webpack の JavaScript ベースエンジンではなく、Rspack の Rust ベースコンパイラが行います。

3つのバンドラーが共存する戦略は、実用的な判断の結果です。Turbopack は将来の姿(最速で Next.js に最適化)、webpack は現在の主力(エコシステムとの互換性が最も高い)、Rspack は現実的な中間点(高速かつ webpack 互換)という位置づけです。この多バンドラー戦略を、サーバーランタイムの重複実装なしに成立させているのが、共通のマニフェスト形式です。

次の記事

ファイルシステムの探索からコード生成、コンパイル、マニフェスト出力まで、ビルドパイプライン全体を追ってきました。最終記事では、システム全体にまたがるキャッシュアーキテクチャを掘り下げます。並行リクエストを重複排除するレスポンスキャッシュ、ISR ページを永続化するインクリメンタルキャッシュ、use cache ディレクティブのキャッシュハンドラーインターフェース、そしてタグベースの再検証システムまで、順を追って解説します。