Read OSS

Next.js コードベースアーキテクチャ:全体像を把握するための地図

中級

前提知識

  • アプリケーション開発者としての Next.js の基本的な知識
  • モノレポ構造とパッケージマネージャーの基本的な理解
  • サーバーサイドレンダリングとクライアントサイドレンダリングの違いの理解

Next.js コードベースアーキテクチャ:全体像を把握するための地図

Next.js のリポジトリは、JavaScript エコシステムでも最大級のオープンソース TypeScript プロジェクトのひとつです。コアパッケージだけでソースファイルが 7,000 以上あり、バンドラーやトランスフォームパイプラインには Rust レイヤーが組み込まれています。初めて触れる人にとって、その規模感は圧倒的かもしれません。この記事は、個々のサブシステムへ踏み込む前に手元へ置いておきたい「地図」です。モノレポがどう整理されているか、何がどこにあるか、そして以降の深掘り記事全体を通して繰り返し登場するアーキテクチャの核心的な概念を解説します。

モノレポのレイアウトとツール構成

Next.js リポジトリは、Turborepo で管理された pnpm ワークスペースです。pnpm-workspace.yaml にワークスペースの設定が定義されており、各パッケージの境界がここで決まります。

graph TD
    Root["nextjs-project (root)"] --> Packages["packages/* (19 npm packages)"]
    Root --> Apps["apps/* (docs, analyzer)"]
    Root --> Crates["crates/* (Rust workspace)"]
    Root --> Turbopack["turbopack/ (vendored)"]
    Root --> Bench["bench/* (benchmarks)"]

    Packages --> Core["next (core framework)"]
    Packages --> SWC["next-swc (native bindings)"]
    Packages --> Rspack["next-rspack"]
    Packages --> CreateApp["create-next-app"]
    Packages --> Font["@next/font"]
    Packages --> ESLint["eslint-plugin-next"]
    Packages --> Others["+ 13 more"]

    Crates --> NapiBind["next-napi-bindings"]
    Crates --> NextCore["next-core"]
    Crates --> CustomTransforms["next-custom-transforms"]
    Crates --> NextBuild["next-build"]

packages/ 配下の 19 パッケージは、それぞれ異なる役割を担っています。

Package Role
next コアフレームワーク — CLI、サーバー、クライアント、ビルドシステム
next-swc Rust への Native N-API バインディング(SWC トランスフォーム + Turbopack)
next-rspack Rspack バンドラーインテグレーション
create-next-app プロジェクトスキャフォールディング CLI
eslint-plugin-next Next.js の規約に沿った ESLint ルール
@next/font フォント最適化
react-refresh-utils React Fast Refresh の HMR サポート
next-codemod 自動マイグレーション codemod

Rust 関連のコードは 2 箇所に分かれています。crates/ には Next.js 固有の Rust クレート(SWC トランスフォーム、Turbopack インテグレーション、NAPI バインディング)が含まれ、turbopack/ は Turbopack バンドラーエンジンのベンダー管理されたコピーです。

ルートの Cargo.toml では next-napi-bindingsnext-corenext-custom-transforms、そして turbopack/crates/* ツリー全体を含む Rust ワークスペースが定義されています。ビルドのオーケストレーションは turbo.json が担い、シンプルな構成です。メインの build タスクは ^build(上流パッケージ)に依存し、出力先は dist/ です。

Tip: リポジトリを探索するなら、まず packages/next/src/ に集中しましょう。200 以上のサンプル、テストスイート、ベンチマークといったその他のコードは、Next.js の仕組みを理解する上では周辺的な存在です。

コアパッケージ:packages/next/src/

フレームワークのロジックの大部分は packages/next パッケージに集約されています。package.json では next バイナリとメインエントリーポイント(./dist/server/next.js)が定義されています。さらに、next/navigationnext/headersnext/imagenext/cache といった公開モジュールを提供する exports マップも含まれています。

src/ ディレクトリは実行コンテキストごとに整理されています。

graph TD
    src["packages/next/src/"] --> cli["cli/ — CLI commands (dev, build, start)"]
    src --> server["server/ — Server runtime (Node.js + Edge)"]
    src --> client["client/ — Browser runtime (router, components)"]
    src --> build["build/ — Build system (webpack config, plugins, loaders)"]
    src --> shared["shared/ — Code shared across all contexts"]
    src --> lib["lib/ — Internal utilities"]

    server --> baseServer["base-server.ts (abstract Server class)"]
    server --> nextServer["next-server.ts (Node.js server)"]
    server --> appRender["app-render/ (App Router rendering)"]
    server --> routeModules["route-modules/ (route handlers)"]

    client --> appRouter["components/app-router.tsx"]
    client --> routerReducer["components/router-reducer/"]
    client --> segmentCache["components/segment-cache/"]

    build --> webpackConfig["webpack-config.ts"]
    build --> plugins["webpack/plugins/"]
    build --> templates["templates/"]

この構成は恣意的なものではありません。Next.js のコードが実際に動作する 3 つの環境に直接対応しています。server/ には Node.js または Edge ランタイムで実行されるコードが、client/ にはブラウザ向けにバンドルされるコードが、build/ にはビルド時に実行され他の 2 つが消費する成果物を生成するコードがあります。shared/ はどのコンテキストからもインポートして安全な型定義、定数、ユーティリティを格納しています。

3 つの実行コンテキスト:Build、Server、Client

Next.js を理解するには、すべてのコードが 3 つのターゲットのいずれかにコンパイルされるという事実を体に染み込ませる必要があります。これは COMPILER_NAMES 定数として定義されています。

export const COMPILER_NAMES = {
  client: 'client',
  server: 'server',
  edgeServer: 'edge-server',
} as const

各コンパイラは、異なるモジュール解決ルール、externals、polyfill を持つ独立したバンドルを生成します。client コンパイラはブラウザ向けの JavaScript(React Client Components を含む)を出力します。server コンパイラは fscrypto など Node.js のフル API にアクセスできるバンドルを生成します。edge-server コンパイラは制限された Edge ランタイム向けのバンドル(Node.js API は使えず V8 のみ)を出力します。

flowchart LR
    Source["Your Source Code"] --> ClientCompiler["Client Compiler"]
    Source --> ServerCompiler["Server Compiler"]
    Source --> EdgeCompiler["Edge Server Compiler"]

    ClientCompiler --> Browser["Browser Bundle\n(React, client components)"]
    ServerCompiler --> NodeJS["Node.js Bundle\n(RSC, API routes, SSR)"]
    EdgeCompiler --> Edge["Edge Bundle\n(Middleware, edge routes)"]

同じソースファイルが複数のコンパイルに登場することもあります。'use client' とマークされたコンポーネントは、サーバーバンドル(クライアント参照として)とクライアントバンドル(実際の実行コードとして)の両方に含まれます。この 3 方向の分割こそが、コードベースが複雑になる根本的な理由であり、server、build、client の各レイヤーに並列するコードパスが現れる理由でもあります。

同じファイルには、各ステージでどの設定が適用されるかを決定するビルドフェーズも定義されています。

export const PHASE_PRODUCTION_BUILD = 'phase-production-build'
export const PHASE_PRODUCTION_SERVER = 'phase-production-server'
export const PHASE_DEVELOPMENT_SERVER = 'phase-development-server'
export const PHASE_TEST = 'phase-test'

これらのフェーズは、next.config.js が関数として定義されている場合に引数として渡され、コンテキストに応じた設定の切り替えを可能にします。コードベース全体でビルド時とランタイムの処理を分岐させるために参照されています。

2 つのルーティングシステムと RouteKind

Next.js は 2 つの完全なルーティングシステムを維持しています。レガシーな Pages Router(pages/)と新しい App Router(app/)で、どちらも同じアプリケーション内で共存できます。フレームワークは RouteKind enum を使って、各ルートがどちらのシステムに属するかを追跡します。

export const enum RouteKind {
  PAGES = 'PAGES',
  PAGES_API = 'PAGES_API',
  APP_PAGE = 'APP_PAGE',
  APP_ROUTE = 'APP_ROUTE',
  IMAGE = 'IMAGE',
}
flowchart TD
    Request["Incoming Request"] --> RouteResolution["Route Resolution"]
    RouteResolution --> PAGES["PAGES\n(pages/*.tsx)"]
    RouteResolution --> PAGES_API["PAGES_API\n(pages/api/*.ts)"]
    RouteResolution --> APP_PAGE["APP_PAGE\n(app/**/page.tsx)"]
    RouteResolution --> APP_ROUTE["APP_ROUTE\n(app/**/route.ts)"]
    RouteResolution --> IMAGE["IMAGE\n(/_next/image)"]

この enum はコードベース全体に登場します。ビルドシステムではユーザーのコードをラップするテンプレートの決定に、サーバーではリクエストを正しいハンドラーにディスパッチするために、キャッシュシステムでは適切な無効化戦略の適用に使われます。コードベースで routeKind のチェックを見かけたら、ほぼ例外なく Pages Router と App Router の動作を分岐させているコードです。

constants.ts にある AdapterOutputType enum は RouteKind を踏まえつつ、PRERENDERSTATIC_FILEMIDDLEWARE などの型を追加しています。これらは Vercel のようなホスティングアダプターが使用するデプロイ出力の種別を表します。

バンドラーの抽象化:Webpack、Turbopack、Rspack

Next.js は CLI フラグまたは設定によって選択できる 3 つのバンドラーをサポートしています。Bundler enum でその選択肢が定義されています。

export enum Bundler {
  Turbopack,
  Webpack,
  Rspack,
}

parseBundlerArgs 関数がバンドラー選択のロジックを担います。このコミット時点では、フラグを指定しない場合のデフォルトは Turbopack です。何も設定されていないとき、74〜76 行目で Bundler.Turbopack が返され TURBOPACK'auto' にセットされています。Webpack を使うには --webpack フラグが必要で、Rspack は NEXT_RSPACK 環境変数または next.config.js で選択します。

flowchart TD
    CLI["CLI Arguments"] --> Parse["parseBundlerArgs()"]
    Env["Environment Variables\n(TURBOPACK, NEXT_RSPACK)"] --> Parse
    Config["next.config.js\n(experimental.rspack)"] --> Finalize["finalizeBundlerFromConfig()"]
    Parse --> Finalize
    Finalize --> Turbopack["Turbopack\n(Rust-based, default)"]
    Finalize --> Webpack["Webpack\n(legacy, --webpack)"]
    Finalize --> Rspack["Rspack\n(NEXT_RSPACK)"]

設計上の重要な注意点として、Rspack の設定は next.config.js 経由で行えますが、このファイルは子プロセスでのみ読み込まれ、メインの CLI プロセスでは読み込まれません。そのため、設定読み込み後に呼ばれる独立したステップとして finalizeBundlerFromConfig() が存在します。ソースコード内のコメントはその事情をなかなか率直に伝えています。「Rspack is configured via next config which is chaotic.」

3 つのバンドラーは、エントリーポイントの収集、テンプレートベースのコード生成、マニフェスト出力という同じビルドパイプラインの抽象化を共有しています。build/webpack-config.ts ファクトリは Webpack 専用(約 2,760 行)で、Turbopack は turbopack/ および crates/next-core の Rust クレートを、Rspack は packages/next-rspack を使用します。

設定システムの概要

Next.js の設定は server/config.tsloadConfig() 経由で読み込まれます。この関数が担うパイプラインは、見た目以上に複雑です。

flowchart TD
    A["File Detection\n(next.config.js/mjs/ts)"] --> B["TypeScript Transpilation\n(if .ts)"]
    B --> C["Phase-Aware Execution\n(config can be a function)"]
    C --> D["Merge with Defaults\n(defaultConfig)"]
    D --> E["Zod Validation\n(config-schema.ts)"]
    E --> F["Normalization\n(images, i18n, rewrites)"]
    F --> G["NextConfigComplete\n(fully resolved)"]

設定ファイルは find-up を使って探索されます。.ts ファイルの場合は評価前に SWC でトランスパイルされます。設定はオブジェクトとして書くか、現在のフェーズ(PHASE_DEVELOPMENT_SERVERPHASE_PRODUCTION_BUILD など)を受け取る関数として書くことができ、コンテキストに応じた条件付き設定が可能です。

config-shared.ts の型定義では、ユーザー向けの NextConfig(フィールドはすべてオプション)と内部用の NextConfigComplete(デフォルト値が適用された完全解決済みの設定)を区別しています。ExperimentalConfig 型は特に広範で、PPR から Turbopack のファイルシステムキャッシュまで、あらゆる実験的機能のフラグを収めています。

Tip: nextConfig にアクセスするコードを読むときは、型が NextConfig なのか NextConfigComplete なのかを確認しましょう。前者はフィールドが undefined になりえますが、後者は完全であることが保証されています。サーバーサイドのコードのほとんどは NextConfigComplete を扱っています。

constants ファイルには、ビルドとランタイムの間のコントラクトを形成するマニフェストのファイル名も定義されています。

export const BUILD_MANIFEST = 'build-manifest.json'
export const PAGES_MANIFEST = 'pages-manifest.json'
export const APP_PATHS_MANIFEST = 'app-paths-manifest.json'
export const PRERENDER_MANIFEST = 'prerender-manifest.json'
export const ROUTES_MANIFEST = 'routes-manifest.json'

これらのマニフェストは 10 種類以上あり、ビルドシステムとサーバーランタイムをつなぐ主要インターフェースです。ビルドが生成し、サーバーがそれを読み取ることで、どのルートが存在するか、どのページがプリレンダリングされているか、どのクライアントバンドルを配信するか、server/client 境界を越えたモジュール参照をどう解決するかを把握します。

コードベースを読み進める

以降の記事で探索するエリアの実用的なディレクトリマップを示します。

Path Lines What It Does
server/base-server.ts ~3,050 抽象 Server クラス — リクエスト処理パイプライン
server/app-render/app-render.tsx ~7,350 App Router レンダリングエンジン — RSC + ストリーミング
build/index.ts ~4,330 ビルドオーケストレーター — コンパイル + 静的生成
build/webpack-config.ts ~2,930 3 つのコンパイラ向け Webpack 設定ファクトリ
client/components/app-router.tsx クライアントサイドルーターのルートコンポーネント
client/components/router-reducer/ Redux ライクなナビゲーション状態マシン
server/lib/router-server.ts トップレベルのリクエストルーター
server/lib/cache-handlers/ use cache キャッシュハンドラーインターフェース

コードベースには一貫したパターンがあります。複雑なサブシステムは多数の小ファイルに分割するのではなく、1 つの大きなファイル(3,000 行を超えることもある)にまとめられています。これは意図的なトレードオフです。関連するロジックを同じ場所に集めることで、1 ファイル内の全体的な流れを把握しやすくする代わりに、個々のファイルの可読性を犠牲にしています。

次のステップ

この地図を手に入れたので、実際の実行パスをたどる準備が整いました。次の記事では next dev コマンドを起点に、CLI 起動からプロセスフォーク、HTTP サーバーの生成、そして階層化されたサーバーアーキテクチャを追い、最終的にひとつの HTTP リクエストが処理パイプライン全体をどう流れるかを見ていきます。