Vite が内部で Rollup を使う理由と仕組み
Vite は開発時の高速さで知られるフロントエンドビルドツールですが、本番ビルドには内部で Rollup を使っています。開発サーバーでは独自のアプローチを取りながら、なぜプロダクションでは Rollup に委ねるのか。その設計判断と内部の仕組みを見ていきます。
Vite の二面構造
Vite のアーキテクチャは、開発時と本番ビルド時でまったく異なるエンジンを使い分ける二面構造になっています。開発時にはブラウザのネイティブ ESM を活用した独自のデベロッパーサーバーが動き、本番ビルドでは Rollup がバンドル処理を担当します。
ブラウザのネイティブ ESM を利用し、ファイル単位でオンデマンドに変換・配信する。バンドルを行わないため、プロジェクトの規模に関係なく起動が高速。
Rollup を使ってすべてのモジュールをバンドルする。Tree Shaking、コード分割、チャンク最適化など、Rollup の成熟した最適化機能をそのまま活用できる。
この使い分けには明確な理由があります。開発時にバンドルが不要なのは、モダンブラウザが ESM の import / export をネイティブに解釈できるからです。一方、本番環境ではネットワークリクエストの数を減らし、コードを最適化する必要があるため、バンドラの出番となります。
開発サーバーの仕組み
Vite の開発サーバーは、従来のバンドラベースの開発ツールとは根本的にアプローチが異なります。Webpack や Parcel はソースコード全体をバンドルしてからサーバーに載せますが、Vite はファイルを個別に変換してブラウザに返します。
ブラウザが import { ref } from 'vue' のようなコードに遭遇すると、そのモジュールを HTTP リクエストでサーバーに要求します。Vite はリクエストを受け取った時点で該当ファイルだけを変換し、レスポンスとして返します。この仕組みにより、プロジェクトに何千ものファイルがあっても、実際にブラウザが要求したファイルだけが処理対象になります。
ブラウザが import 文を解釈し、モジュールを HTTP リクエストで要求する
Vite の開発サーバーが該当ファイルだけを変換して返す
ブラウザが受け取ったモジュールを実行し、さらに import があれば再びリクエストする
この仕組みの裏側で動いているのが esbuild です。Vite は開発時のファイル変換(TypeScript や JSX のトランスパイル)に esbuild を使っており、Go で書かれた esbuild の圧倒的な速度が Vite の高速な開発体験を支えています。
依存関係の事前バンドル
開発時にバンドルしないと言いましたが、ひとつ例外があります。node_modules 内のサードパーティライブラリは、起動時に esbuild で事前バンドル(Pre-bundling)されます。
React や Lodash など、CommonJS で配布されているパッケージはブラウザのネイティブ ESM では読み込めない。esbuild がこれらを ESM に変換することで、開発サーバー上で動作可能にしている。
lodash-es のように内部で数百のファイルに分かれているパッケージをそのまま読み込むと、ブラウザが大量の HTTP リクエストを発行してしまう。事前に 1 つのファイルにまとめることで、この問題を回避している。
事前バンドルの結果は node_modules/.vite にキャッシュされ、依存関係が変わらない限り再利用されます。そのため 2 回目以降の起動はさらに高速になります。
なぜ本番ビルドに esbuild ではなく Rollup を使うのか
開発時にあれほど高速な esbuild があるのに、なぜ本番ビルドでは Rollup を採用しているのでしょうか。Vite の作者である Evan You 自身がこの判断について説明しており、その理由はいくつかあります。
esbuild は高速ですが、本番ビルドに必要な高度な最適化において Rollup に及ばない部分があります。特にコード分割の柔軟性が大きな差です。Rollup は manualChunks オプションによる細かいチャンク制御や、動的 import に基づく自動的なコード分割を長年にわたって磨いてきました。
また、Rollup のプラグインエコシステムは非常に成熟しており、Vite は Rollup のプラグインインターフェースをそのまま拡張する形で自身のプラグイン API を設計しています。
Rollup プラグインの resolveId、load、transform などのフック関数群。Vite 固有のフックを追加しつつ、既存の Rollup プラグインとの互換性を保っている。
つまり Rollup プラグインの多くが Vite でもそのまま動作します。esbuild に切り替えると、この豊富なプラグイン資産との互換性が失われてしまいます。
Vite のプラグインシステムと Rollup の関係
Vite のプラグインは Rollup のプラグインインターフェースを拡張したものです。Rollup のフックに加えて、Vite 独自のフックが追加されています。
// Vite プラグインの例
export default function myPlugin() {
return {
name: 'my-plugin',
// Rollup 互換フック(開発・ビルド両方で動く)
resolveId(source) {
if (source === 'virtual:config') {
return source;
}
},
load(id) {
if (id === 'virtual:config') {
return 'export const debug = true;';
}
},
// Vite 独自フック(開発サーバー専用)
configureServer(server) {
server.middlewares.use('/api', (req, res) => {
res.end('hello');
});
}
};
}resolveId や load、transform といった Rollup 由来のフックは開発時にも本番ビルド時にも呼ばれます。一方、configureServer のような Vite 独自フックは開発サーバーでのみ有効です。この設計により、ひとつのプラグインで開発と本番の両方に対応でき、かつ Rollup エコシステムの資産を活かせるようになっています。
vite.config.js の build オプション
本番ビルドの設定は vite.config.js の build フィールドで行いますが、ここで指定するオプションの多くは Rollup にそのまま渡されます。
import { defineConfig } from 'vite';
export default defineConfig({
build: {
// Rollup にそのまま渡るオプション
rollupOptions: {
input: {
main: 'index.html',
admin: 'admin.html'
},
output: {
manualChunks: {
vendor: ['react', 'react-dom']
}
}
},
// Vite 側で制御するオプション
target: 'es2020',
minify: 'terser',
sourcemap: true
}
});rollupOptions の中身はそのまま Rollup の設定として解釈されるため、Rollup のドキュメントを参照しながら細かいチューニングが可能です。Vite はこの上に target や minify といった高レベルのオプションをかぶせて、一般的なユースケースをシンプルに設定できるようにしています。
Rolldown という未来
Vite の今後を語るうえで触れておくべきなのが Rolldown の存在です。Rolldown は Rollup と互換性を持ちながら Rust で書き直されたバンドラで、Vite チームが主導して開発を進めています。
開発時は esbuild、本番ビルドは Rollup という二面構造。両者の挙動の違いが開発と本番で微妙な差異を生むことがある。
開発時も本番ビルド時も同じエンジン(Rolldown)で統一される。Rust による高速性と Rollup 互換のプラグイン API を両立し、環境間の差異が解消される見込み。
Rolldown が安定すれば、Vite は esbuild と Rollup という 2 つの外部依存を Rolldown ひとつに置き換えられます。開発と本番で同じバンドラが動くことで、環境差に起因するバグが減り、ビルドパイプライン全体がシンプルになることが期待されています。
現時点での Vite は、開発時の速度を esbuild で、本番ビルドの品質を Rollup で確保するという実利的な設計を取っています。それぞれのツールが最も得意な領域を担当させるこのアプローチは、単一ツールの限界を補い合う合理的な判断であり、Vite が急速に普及した要因のひとつといえるでしょう。