プロダクションビルド、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 のようなメタフレームワークは、このフックを使って次のような処理を実現できます。
- まずクライアント環境をビルドする
- クライアントマニフェストを読み込む
- クライアントチャンクへの参照を持つ SSR 環境をビルドする
- 出力ディレクトリを調整する
フックの実行順序は他のフックと同様に 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/experimental の viteBuildImportAnalysisPlugin)に処理を委譲しています。
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 クラスです。
ESModulesEvaluator は AsyncFunction を使って変換後のコードを実行します。
const initModule = new AsyncFunction(
ssrModuleExportsKey,
ssrImportMetaKey,
ssrImportKey,
ssrDynamicImportKey,
ssrExportAllKey,
ssrExportNameKey,
'"use strict";' + code,
)
vm.runInNewContext ではなく AsyncFunction を採用した理由は、Node.js に限らずブラウザ、Deno、Cloudflare Workers といった任意の JavaScript 環境で動作するからです。評価後にモジュールのエクスポートは Object.seal() で封印され、意図しない変更が防止されます。
ヒント:
ESModulesEvaluatorのstartOffsetプロパティは、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 のようなビルド専用フックはビルド時にのみ実行されます。しかし resolveId → load → transform というコアパイプラインは両モードで共有されています。
Vite 8 の Rolldown 移行により、開発とビルドの差はさらに縮まっています。Rolldown のネイティブリゾルバーが両モードを支えているのです。第 5 回で紹介した実験的なフルバンドル開発モードではその区別が完全に消え、プロダクションと同様にバンドルされた出力を開発時にもそのまま配信します。
シリーズのまとめ
この6回のシリーズを通じて、Vite 8 の 79 行の CLI ブートストラップから始まり、設定の解決、18 層のミドルウェアスタック、28 以上のコアプラグインを持つプラグインシステムを解説しました。さらに HMR の境界伝播、依存関係の事前バンドル、プロダクションビルド、そして module runner まで追いかけてきました。得られた重要な洞察をまとめます。
- 4つのランタイムコンテキスト(node、client、module-runner、shared)がクリーンな分離を強制し、クロスプラットフォームの実行を可能にする
- 環境の階層構造により、各環境が独自のモジュールグラフ、プラグインコンテナ、オプティマイザーを持ちながら、Proxy ベースの設定マージで重複を避けている
- Rolldown がツールチェーンを統一した。esbuild と Rollup の分担を廃止し、開発時のトランスフォームとプロダクションビルドの両方を単一の Rust ベースバンドラーで担う
- プラグインシステムが拡張ポイントである。28 以上のコアプラグインが Rolldown 互換フックを通じて、解決・CSS・アセット・HTML・インポートなどの処理を組み合わせている
- HMR と事前バンドルは深く統合されている。モジュールグラフ、ファイルウォッチャー、WebSocket チャンネルが連携してサブセカンドの更新を実現する
Vite プラグインを作る場合でも、Vite コアにコントリビュートする場合でも、トリッキーなトランスフォームの問題をデバッグする場合でも、このコードベースの地図が道標になれば幸いです。