Read OSS

プロダクションビルド、ViteBuilder、そして Module Runner

上級

前提知識

  • 第 1 回: アーキテクチャとコードベースの全体像
  • 第 2 回: 設定と環境システム
  • 第 3 回: Dev サーバーとトランスフォームパイプライン
  • 第 4 回: プラグインシステムとコアプラグイン
  • 第 5 回: HMR と依存関係の最適化

プロダクションビルド、ViteBuilder、そして Module Runner

Vite において、開発環境とプロダクション環境は根本的に異なります。開発時はブラウザのリクエストに応じてモジュールをオンデマンドで変換しますが、プロダクションではすべてをバンドルし、ツリーシェイキング・ミニファイを施してディスクに書き出します。それでも両モードは同じプラグインシステムと設定の大部分を共有しています。

このシリーズ最終回では、残る3つのサブシステムを取り上げます。Rolldown を使って最適化された出力を生成する build() パイプライン、マルチ環境ビルドを調整する ViteBuilder、そして任意の JavaScript ランタイムでサーバーサイドコードを評価する ModuleRunner です。

build() 関数と BuildEnvironment

プロダクションビルドは build() 関数から始まります。第 1 回で触れたように、CLI からは createBuilder 経由で呼び出されます。実際の処理は buildEnvironment で行われます。

sequenceDiagram
    participant CLI
    participant createBuilder
    participant buildEnvironment
    participant Rolldown

    CLI->>createBuilder: createBuilder(inlineConfig)
    createBuilder->>createBuilder: resolveConfig(inlineConfig, 'build')
    createBuilder->>createBuilder: Create BuildEnvironment per env
    createBuilder->>buildEnvironment: builder.build(environment)
    buildEnvironment->>buildEnvironment: resolveRolldownOptions(environment)
    buildEnvironment->>Rolldown: rolldown(rollupOptions)
    Rolldown-->>buildEnvironment: RolldownBuild
    buildEnvironment->>Rolldown: bundle.write(outputOptions)
    Rolldown-->>buildEnvironment: RolldownOutput
    buildEnvironment-->>CLI: Output files

BuildEnvironment クラスは BaseEnvironment を継承し、mode: 'build' フラグと isBuilt トラッカーを追加しています。モジュールグラフもホットチャンネルもリクエストの追跡機構も持たないため、DevEnvironment と比べて意図的にシンプルな設計になっています。

resolveRolldownOptions 関数は、Vite の設定を Rolldown の RolldownOptions に変換します。具体的には、HTML ファイル(アプリモード)や設定ファイル(ライブラリモード)からエントリーポイントを解決し、出力オプション(フォーマット、チャンク名、アセットのインライン閾値)を設定し、プラグインパイプラインを統合する処理が含まれます。

792〜860 行目buildEnvironment 関数は、rolldown() を呼び出してビルドを作成し、通常ビルドでは bundle.write()--watch モードでは Rolldown ネイティブのファイルウォッチャーを使ったウォッチャーをセットアップします。ウォッチャーには Vite の設定から解決した chokidar オプションが渡されます。

ViteBuilder とマルチ環境ビルド

createBuilder 関数は、複数の環境をビルドできる ViteBuilder を生成します。中心となるのは 1807〜1841 行目buildApp() メソッドです。

sequenceDiagram
    participant Framework
    participant buildApp
    participant Plugins
    participant configBuilder

    buildApp->>Plugins: Run 'pre' and 'normal' buildApp hooks
    Plugins-->>buildApp: (may build some environments)
    buildApp->>configBuilder: configBuilder.buildApp(builder)
    configBuilder-->>buildApp: (default: no-op)
    buildApp->>Plugins: Run 'post' buildApp hooks
    buildApp->>buildApp: Any environments not built?
    alt Some environments unbuilt
        buildApp->>buildApp: Build remaining environments sequentially
    end

buildApp プラグインフックは、フレームワークがビルドのオーケストレーションを制御するための仕組みです。Nuxt や SolidStart のようなメタフレームワークは、このフックを使って次のような処理を実現できます。

  1. まずクライアント環境をビルドする
  2. クライアントマニフェストを読み込む
  3. クライアントチャンクへの参照を持つ SSR 環境をビルドする
  4. 出力ディレクトリを調整する

フックの実行順序は他のフックと同様に enforce/order のソートに従います。order: 'pre' のフックと通常のフックが先に実行され、次に configBuilder.buildApp(ユーザーのビルダー設定)、最後に order: 'post' のフックが実行されます。

1832〜1840 行目 のフォールバック処理により、buildApp フックがどの環境もビルドしなかった場合は、すべての環境が順次ビルドされます。

if (Object.values(builder.environments).every(
  (environment) => !environment.isBuilt,
)) {
  for (const environment of Object.values(builder.environments)) {
    await builder.build(environment)
  }
}

環境ごとの設定を分離するために、createBuilder はオプションで環境ごとに設定を個別に解決します(デフォルトの sharedConfigBuild: false の場合)。これにより、プラグインがビルドごとにフレッシュなインスタンスを受け取れるようになり、「プラグインは一度に一つのバンドルを処理する」というエコシステムの期待に沿った動作が保証されます。

ビルド時のインポート解析

buildImportAnalysisPlugin は、開発時のものとは異なるアプローチでインポートを処理します。開発時はタイムスタンプクエリの付与や最適化済み依存関係へのリダイレクトが中心でしたが、ビルド時の関心事は次のように変わります。

flowchart TD
    A["Parse imports with es-module-lexer"] --> B{"Dynamic import?"}
    B -->|Yes| C["Insert __vitePreload wrapper"]
    B -->|No| D["Leave as static import"]
    C --> E["Collect CSS deps for preload"]
    E --> F["Generate preload directive"]
    F --> G["Replace __VITE_PRELOAD__ marker in generateBundle"]

プリロードの仕組みはパフォーマンス上とても重要です。チャンク A がチャンク B を動的インポートし、チャンク B が CSS ファイルをインポートする場合、プリロードディレクティブによって CSS がチャンク B と並行してフェッチされます。ウォーターフォールでの読み込みを防ぐためです。__VITE_PRELOAD__ マーカーは transform フェーズで挿入され、チャンクグラフ全体が確定する generateBundle フェーズで実際のチャンクパスに置き換えられます。

また、このビルドプラグインは解析の一部を Rust で実行するために、Rolldown ネイティブのプラグイン(rolldown/experimentalviteBuildImportAnalysisPlugin)に処理を委譲しています。

ModuleRunner システム

ModuleRunner は、任意の JavaScript ランタイムでサーバーサイドコードを実行するための Vite の仕組みです。SSR の基盤となっており、Node.js、Worker、エッジランタイムのいずれでも動作します。

graph TD
    subgraph "ModuleRunner"
        MR["ModuleRunner"] --> EM["EvaluatedModules<br/>(module cache)"]
        MR --> TR["Transport<br/>(to DevEnvironment)"]
        MR --> HMR["HMRClient<br/>(optional)"]
        MR --> EV["ModuleEvaluator"]
    end

    subgraph "ESModulesEvaluator"
        EV --> AF["new AsyncFunction(<br/>  __vite_ssr_exports__,<br/>  __vite_ssr_import_meta__,<br/>  __vite_ssr_import__,<br/>  ...<br/>  code<br/>)"]
    end

    TR -->|"fetchModule(id)"| DEV["DevEnvironment<br/>(Vite Server)"]
    DEV -->|"transform + ssrTransform"| TR

53〜79 行目 のコンストラクターは、オプション、オプションのエバリュエーター(デフォルトは ESModulesEvaluator)、オプションのデバッガーを受け取ります。HMR が有効な場合は、src/shared/hmr.ts の共有実装を使って HMRClient を生成します。これはブラウザクライアントが使うものと同じ HMRClient クラスです。

ESModulesEvaluatorAsyncFunction を使って変換後のコードを実行します。

const initModule = new AsyncFunction(
  ssrModuleExportsKey,
  ssrImportMetaKey,
  ssrImportKey,
  ssrDynamicImportKey,
  ssrExportAllKey,
  ssrExportNameKey,
  '"use strict";' + code,
)

vm.runInNewContext ではなく AsyncFunction を採用した理由は、Node.js に限らずブラウザ、Deno、Cloudflare Workers といった任意の JavaScript 環境で動作するからです。評価後にモジュールのエクスポートは Object.seal() で封印され、意図しない変更が防止されます。

ヒント: ESModulesEvaluatorstartOffset プロパティは、AsyncFunction ラッパーによって生じる行オフセットを吸収するためのものです。これにより、評価されたモジュールからエラーが投げられたときでもソースマップが正しく対応します。

トランスポート層とクロス環境通信

ModuleRunnerTransport インターフェースは、module runner と Vite サーバー間の通信を抽象化します。

export interface ModuleRunnerTransport {
  connect?(handlers: ModuleRunnerTransportHandlers): Promise<void> | void
  disconnect?(): Promise<void> | void
  send?(data: HotPayload): Promise<void> | void
  invoke?(data: HotPayload): Promise<{ result: any } | { error: any }>
  timeout?: number
}

通信モデルは二種類あります。send/connect(メッセージベース、WebSocket や Worker のメッセージ向け)と invoke(RPC スタイル、同一プロセス内通信向け)です。normalizeModuleRunnerTransport 関数はどちらのモデルも、型付き RPC メソッドを持つ統一された InvokeableModuleRunnerTransport にラップします。

graph LR
    subgraph "Same Process"
        RUNNER1["ModuleRunner"] -->|"invoke()"| DEV1["DevEnvironment"]
    end
    subgraph "Worker Thread"
        RUNNER2["ModuleRunner"] -->|"send/connect"| MSG["MessagePort"] -->|"send/connect"| DEV2["DevEnvironment"]
    end
    subgraph "Remote (Edge)"
        RUNNER3["ModuleRunner"] -->|"send/connect"| WS["WebSocket"] -->|"send/connect"| DEV3["DevEnvironment"]
    end

send/connect モデルでは、43〜80 行目 のトランスポート実装が nanoid で生成したリクエスト ID と保留中の Promise を管理する Map を使って RPC 層を構築します。レスポンスは ID で照合され、タイムアウトも設定可能です。

開発モードとビルドモードの比較

シリーズの締めくくりとして、Vite の二つのモードでモジュールがどのように処理されるかを並べて確認しましょう。

観点 開発 (transformRequest) ビルド (Rolldown bundle)
エントリー ブラウザの HTTP リクエスト 設定のエントリーポイント / HTML
解決 プラグインコンテナの resolveId Rolldown ネイティブ + プラグインの resolveId
ロード プラグインコンテナの load → fs フォールバック Rolldown ネイティブ + プラグインの load
トランスフォーム プラグインコンテナの transform(逐次) Rolldown + プラグインの transform(並列)
出力 単一モジュールのレスポンス バンドル済みチャンク、ツリーシェイク済み
モジュールグラフ EnvironmentModuleGraph(環境ごと) Rolldown 内部グラフ
インポート importAnalysisPlugin で書き換え buildImportAnalysisPlugin で解決
CSS <style> タグで注入 .css ファイルとして抽出
依存関係 オプティマイザーで事前バンドル インラインバンドル or 分割
HMR WebSocket → 再インポート なし(ウォッチモードで再ビルド)

両モードの収束点はプラグインシステムです。同じプラグインが両モードで動作し、同じフックが呼び出されます。hotUpdate のような Vite 固有のフックは本質的に開発時専用であり、generateBundle のようなビルド専用フックはビルド時にのみ実行されます。しかし resolveIdloadtransform というコアパイプラインは両モードで共有されています。

Vite 8 の Rolldown 移行により、開発とビルドの差はさらに縮まっています。Rolldown のネイティブリゾルバーが両モードを支えているのです。第 5 回で紹介した実験的なフルバンドル開発モードではその区別が完全に消え、プロダクションと同様にバンドルされた出力を開発時にもそのまま配信します。

シリーズのまとめ

この6回のシリーズを通じて、Vite 8 の 79 行の CLI ブートストラップから始まり、設定の解決、18 層のミドルウェアスタック、28 以上のコアプラグインを持つプラグインシステムを解説しました。さらに HMR の境界伝播、依存関係の事前バンドル、プロダクションビルド、そして module runner まで追いかけてきました。得られた重要な洞察をまとめます。

  1. 4つのランタイムコンテキスト(node、client、module-runner、shared)がクリーンな分離を強制し、クロスプラットフォームの実行を可能にする
  2. 環境の階層構造により、各環境が独自のモジュールグラフ、プラグインコンテナ、オプティマイザーを持ちながら、Proxy ベースの設定マージで重複を避けている
  3. Rolldown がツールチェーンを統一した。esbuild と Rollup の分担を廃止し、開発時のトランスフォームとプロダクションビルドの両方を単一の Rust ベースバンドラーで担う
  4. プラグインシステムが拡張ポイントである。28 以上のコアプラグインが Rolldown 互換フックを通じて、解決・CSS・アセット・HTML・インポートなどの処理を組み合わせている
  5. HMR と事前バンドルは深く統合されている。モジュールグラフ、ファイルウォッチャー、WebSocket チャンネルが連携してサブセカンドの更新を実現する

Vite プラグインを作る場合でも、Vite コアにコントリビュートする場合でも、トリッキーなトランスフォームの問題をデバッグする場合でも、このコードベースの地図が道標になれば幸いです。