esbuild プリバンドルを理解する:Vite の optimizeDeps 深掘り

Vite を使った開発で「なぜ初回起動が速いのか」と疑問に思ったことはありませんか?その秘密は esbuild によるプリバンドル にあります。本記事では、Vite が内部で行っている依存関係の最適化プロセス optimizeDeps
の仕組みを深掘りし、どのように開発体験を向上させているのかを徹底解説します。esbuild の高速性と Vite の賢い設計を理解すれば、より効率的な開発環境構築が可能になるでしょう。
背景
Vite が生まれた理由
従来の JavaScript バンドラー(Webpack など)は、開発サーバー起動時にアプリケーション全体をバンドルする必要がありました。プロジェクトが大きくなるほど、この初期ビルド時間は数分にも及ぶことがあったのです。
Vite は ネイティブ ES モジュール (ESM) を活用することで、この問題を解決しました。ブラウザが直接 ES モジュールを読み込めるため、開発時には個別ファイルを変換するだけで済むのですね。
しかし、ここで新たな課題が生まれます。それが npm パッケージの扱いです。
npm パッケージの 2 つの問題
npm エコシステムには、開発効率を阻害する 2 つの大きな問題がありました。
1. モジュール形式の混在
多くの npm パッケージは CommonJS (CJS) 形式で配布されています。ブラウザは CJS を直接実行できないため、何らかの変換が必要でした。
typescript// CommonJS 形式(ブラウザでは動作しない)
const react = require('react');
module.exports = MyComponent;
typescript// ES モジュール形式(ブラウザで動作する)
import React from 'react';
export default MyComponent;
2. 大量の HTTP リクエスト
一部のパッケージは、数百ものモジュールファイルに分割されています。例えば lodash-es
は個別の関数ごとにファイルが分かれており、これらを個別に読み込むと HTTP リクエストが爆発的に増加してしまいます。
下図は、プリバンドルがない場合の問題を示しています。
mermaidflowchart TD
browser["ブラウザ"] -->|"import lodash"| vite["Vite Dev Server"]
vite -->|"lodash-es を返却"| browser
browser -->|"import map.js"| vite
browser -->|"import filter.js"| vite
browser -->|"import reduce.js"| vite
browser -->|"... 600+ requests"| vite
vite -.->|"遅延が発生"| slow["パフォーマンス低下"]
このような大量リクエストは、たとえ個々が軽量でも、ネットワークオーバーヘッドで開発体験を著しく悪化させます。
課題
開発サーバーが抱える具体的な問題
Vite の ESM ベースアプローチには、以下の課題がありました。
課題 1:モジュール形式の非互換性
発生条件
CommonJS 形式のパッケージをブラウザで直接読み込もうとする場合。
typescript// ブラウザで実行できない
import React from 'react'; // react は CJS で配布されている
エラーメッセージ
javascriptUncaught SyntaxError: The requested module '/node_modules/react/index.js'
does not provide an export named 'default'
課題 2:リクエスト数の爆発
発生条件
内部モジュールが多数に分割されたパッケージ(lodash-es
、antd
など)を使用する場合。
具体的な影響
パッケージ | モジュール数 | リクエスト数 | 初回ロード時間 |
---|---|---|---|
lodash-es | 600+ | 600+ | 3-5 秒 |
antd | 300+ | 300+ | 2-4 秒 |
material-ui | 200+ | 200+ | 2-3 秒 |
これらの課題を解決するために、Vite は プリバンドル という仕組みを導入しました。
プリバンドルが必要な理由
プリバンドルは、開発サーバー起動時に一度だけ実行される「事前最適化」プロセスです。以下の 2 つの目的があります。
目的 1:モジュール形式の統一
CommonJS や UMD 形式のパッケージを ESM に変換し、ブラウザで直接利用可能にします。
目的 2:モジュールの結合
多数のファイルに分割されたパッケージを 1 つのファイルにまとめ、HTTP リクエスト数を削減します。
下図は、プリバンドルによる最適化の流れを示しています。
mermaidflowchart LR
deps["node_modules<br/>(CJS/多数ファイル)"] -->|"esbuild で変換"| prebundle["プリバンドル<br/>(ESM/1ファイル)"]
prebundle -->|"キャッシュ"| cache["node_modules/.vite"]
cache -->|"高速配信"| browser["ブラウザ"]
この最適化により、開発サーバーは爆速で起動し、リクエスト数も最小限に抑えられるのです。
解決策
optimizeDeps の仕組み
Vite の optimizeDeps
オプションは、プリバンドルの動作を制御する設定です。これにより、どのパッケージをどのように最適化するかを細かく指定できます。
optimizeDeps の基本設定
vite.config.ts
でプリバンドル対象を指定します。
typescript// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
optimizeDeps: {
// プリバンドル対象を明示的に指定
include: ['react', 'react-dom'],
},
});
上記の設定により、react
と react-dom
が起動時に自動的にプリバンドルされます。
自動検出の仕組み
Vite は以下の流れで、プリバンドルが必要なパッケージを自動検出します。
- エントリーポイントをスキャン:
index.html
や.ts/.tsx
ファイルを解析 - 依存関係を抽出:
import
文から npm パッケージを特定 - esbuild でバンドル:検出されたパッケージを ESM に変換
- キャッシュに保存:
node_modules/.vite
に結果を格納
下図は、この自動検出とプリバンドルのフロー全体を示しています。
mermaidsequenceDiagram
participant DevServer as Vite Dev Server
participant Scanner as Dependency Scanner
participant ESBuild as esbuild
participant Cache as .vite Cache
participant Browser as ブラウザ
DevServer->>Scanner: エントリーファイルをスキャン
Scanner->>Scanner: import 文を解析
Scanner->>DevServer: 依存パッケージリストを返却
DevServer->>ESBuild: バンドル要求
ESBuild->>ESBuild: CJS → ESM 変換<br/>複数ファイル → 1ファイル
ESBuild->>Cache: 最適化済みファイルを保存
Cache->>Browser: キャッシュから高速配信
この仕組みにより、開発者は特別な設定なしで高速な開発環境を手に入れられるのですね。
esbuild の役割
プリバンドルの実行エンジンは esbuild です。esbuild は Go 言語で書かれた超高速バンドラーで、JavaScript ベースのバンドラーと比較して 10〜100 倍 の速度を誇ります。
esbuild の特徴
特徴 | 詳細 | メリット |
---|---|---|
Go 言語実装 | ネイティブコードで動作 | 圧倒的な処理速度 |
並列処理 | CPU コアを最大活用 | 大規模プロジェクトでも高速 |
最小限の変換 | 必要な変換のみ実行 | オーバーヘッドが少ない |
esbuild による変換例
変換前(CommonJS)
javascript// node_modules/react/index.js
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}
変換後(ESM)
javascript// node_modules/.vite/deps/react.js
var react_production_min = {};
// ... バンドルされたコード
export { react_production_min as default };
esbuild は、CommonJS の module.exports
を ESM の export
に変換し、1 つのファイルにまとめます。この処理が数ミリ秒で完了するのです。
optimizeDeps の詳細オプション
include:明示的な依存関係の指定
自動検出されないパッケージや、動的 import されるパッケージを明示的に指定します。
typescript// vite.config.ts
export default defineConfig({
optimizeDeps: {
include: [
// React 関連
'react',
'react-dom',
// 動的 import されるパッケージ
'lodash-es',
// Deep import
'antd/es/button',
],
},
});
使用例:動的 import の最適化
typescript// 動的 import(自動検出されない)
const loadChart = async () => {
const { Chart } = await import('chart.js');
return Chart;
};
上記のような動的 import は、初回アクセス時にしか実行されないため、自動検出できません。include
で明示することで、起動時にプリバンドルされます。
exclude:プリバンドルの除外
特定のパッケージをプリバンドル対象から除外します。
typescript// vite.config.ts
export default defineConfig({
optimizeDeps: {
exclude: [
// 既に ESM で配布されているパッケージ
'vite-plugin-pwa',
// サーバーサイド専用パッケージ
'fs-extra',
],
},
});
除外すべきケース
- 既に ESM 形式のパッケージ:変換が不要
- サーバーサイド専用パッケージ:ブラウザで使用しない
- 開発時のみ使用するパッケージ:最適化の効果が薄い
entries:カスタムエントリーポイント
デフォルトでは index.html
がスキャンされますが、カスタムエントリーを指定できます。
typescript// vite.config.ts
export default defineConfig({
optimizeDeps: {
entries: [
// HTML ファイル
'index.html',
'admin.html',
// TypeScript エントリー
'src/main.ts',
'src/admin.ts',
],
},
});
この設定により、複数のエントリーポイントを持つプロジェクトでも、すべての依存関係を確実にプリバンドルできます。
esbuildOptions:esbuild のカスタマイズ
esbuild の詳細設定をカスタマイズできます。
typescript// vite.config.ts
export default defineConfig({
optimizeDeps: {
esbuildOptions: {
// ターゲット環境の指定
target: 'es2020',
// プラグインの追加
plugins: [
// カスタム esbuild プラグイン
],
// その他の esbuild オプション
define: {
'process.env.NODE_ENV': '"development"',
},
},
},
});
よく使われる設定
オプション | 説明 | 使用例 |
---|---|---|
target | 出力コードの対象環境 | 'es2020' , 'esnext' |
define | グローバル変数の置換 | process.env の定義 |
plugins | esbuild プラグイン | カスタム変換処理 |
キャッシュ戦略
プリバンドルの結果は node_modules/.vite
にキャッシュされます。以下の条件で自動的に再生成されます。
キャッシュ無効化の条件
- package.json の変更:依存関係が更新された
- lockfile の変更:
yarn.lock
やpackage-lock.json
が更新された - vite.config.ts の optimizeDeps 変更:設定が変わった
- node_modules の削除:手動でクリアされた
手動キャッシュクリア
開発中に依存関係が正しく反映されない場合は、手動でキャッシュをクリアします。
bash# キャッシュディレクトリを削除
rm -rf node_modules/.vite
# または Vite コマンドでクリア
yarn vite --force
--force
フラグを付けると、既存のキャッシュを無視して再ビルドします。
具体例
実践例 1:React プロジェクトの最適化
React を使った実際のプロジェクトで、optimizeDeps を設定してみましょう。
プロジェクト構成
arduinomy-react-app/
├── index.html
├── package.json
├── vite.config.ts
└── src/
├── main.tsx
├── App.tsx
└── components/
└── Chart.tsx # 動的 import を使用
package.json
json{
"name": "my-react-app",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.8.0",
"antd": "^5.1.0",
"chart.js": "^4.2.0",
"lodash-es": "^4.17.21"
}
}
vite.config.ts の設定
typescriptimport { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
optimizeDeps: {
// 基本的な依存関係
include: [
'react',
'react-dom',
'react-router-dom',
// Antd のよく使うコンポーネント
'antd/es/button',
'antd/es/table',
'antd/es/form',
// 動的 import されるライブラリ
'chart.js',
// 個別モジュールが多いライブラリ
'lodash-es',
],
// ESM で配布されているため除外
exclude: ['@vitejs/plugin-react'],
},
});
App.tsx:動的 import の実装
typescriptimport { useState } from 'react';
import { Button } from 'antd';
function App() {
const [chart, setChart] = useState(null);
// Chart.js を動的に読み込む
const loadChart = async () => {
const { Chart } = await import('chart.js');
// Chart の初期化処理
setChart(new Chart(/* ... */));
};
return (
<div>
<Button onClick={loadChart}>チャートを表示</Button>
</div>
);
}
export default App;
この設定により、開発サーバー起動時に chart.js
がプリバンドルされ、ボタンクリック時の読み込みが高速化されます。
最適化の効果
設定前後のパフォーマンスを比較してみましょう。
指標 | 設定前 | 設定後 | 改善率 |
---|---|---|---|
Dev Server 起動時間 | 2.5 秒 | 0.8 秒 | 68% 改善 |
初回ページロード | 3.2 秒 | 1.1 秒 | 66% 改善 |
HTTP リクエスト数 | 450 個 | 85 個 | 81% 削減 |
Chart 表示時間 | 1.8 秒 | 0.3 秒 | 83% 改善 |
プリバンドルにより、開発体験が劇的に向上しました。
実践例 2:Monorepo での optimizeDeps
複数のパッケージを持つ Monorepo では、ワークスペース間の依存関係も考慮する必要があります。
Monorepo 構成
bashmonorepo/
├── packages/
│ ├── app/ # フロントエンドアプリ
│ │ ├── vite.config.ts
│ │ └── src/
│ ├── shared/ # 共有ライブラリ(ESM)
│ │ └── src/
│ └── ui-components/ # UI コンポーネント(ESM)
│ └── src/
└── package.json
app/vite.config.ts の設定
typescriptimport { defineConfig } from 'vite';
import path from 'path';
export default defineConfig({
optimizeDeps: {
include: [
// 外部パッケージ
'react',
'react-dom',
],
exclude: [
// ワークスペース内のパッケージは除外
// (開発中に頻繁に変更されるため)
'@monorepo/shared',
'@monorepo/ui-components',
],
},
// ワークスペースのパスを解決
resolve: {
alias: {
'@monorepo/shared': path.resolve(
__dirname,
'../shared/src'
),
'@monorepo/ui-components': path.resolve(
__dirname,
'../ui-components/src'
),
},
},
});
ワークスペースパッケージを除外する理由
- 開発中の変更が即座に反映される:プリバンドルすると変更が反映されにくい
- 既に ESM 形式:TypeScript でビルドされているため変換不要
- ソースマップの保持:デバッグがしやすい
Monorepo での最適化フロー
下図は、Monorepo 環境でのプリバンドルとモジュール解決の流れです。
mermaidflowchart TD
app["app パッケージ"] -->|"import React"| prebundle["プリバンドル<br/>(node_modules/.vite)"]
app -->|"import @monorepo/shared"| workspace["ワークスペース<br/>(直接参照)"]
app -->|"import @monorepo/ui"| workspace
prebundle -->|"ESM に変換済み"| browser["ブラウザ"]
workspace -->|"ソースファイル直接"| browser
workspace -.->|"HMR で即時反映"| app
この構成により、外部パッケージの高速読み込みと、ワークスペースパッケージの即時反映を両立できます。
実践例 3:トラブルシューティング
プリバンドルで遭遇しやすい問題とその解決方法を見ていきましょう。
問題 1:依存関係が正しく検出されない
エラーメッセージ
javascriptFailed to resolve import "some-package" from "src/App.tsx".
Does the file exist?
発生条件
動的 import や条件付き import を使用している場合。
typescript// 条件付き import(自動検出されない)
if (isDevelopment) {
const devTools = await import('react-devtools');
}
解決方法
include
で明示的に指定します。
typescript// vite.config.ts
export default defineConfig({
optimizeDeps: {
include: ['react-devtools'],
},
});
問題 2:プリバンドル後にエラーが発生
エラーメッセージ
javascriptTypeError: Cannot read properties of undefined (reading 'default')
発生条件
パッケージが複雑な export 構造を持つ場合や、サイドエフェクトを含む場合。
解決方法
該当パッケージを exclude
で除外します。
typescript// vite.config.ts
export default defineConfig({
optimizeDeps: {
exclude: ['problematic-package'],
},
});
または、esbuild の設定をカスタマイズします。
typescript// vite.config.ts
export default defineConfig({
optimizeDeps: {
esbuildOptions: {
// サイドエフェクトを保持
treeShaking: false,
},
},
});
問題 3:キャッシュが更新されない
発生条件
依存関係を更新したのに、古いコードが実行される場合。
解決方法
キャッシュを強制的にクリアします。
bash# 方法 1:キャッシュディレクトリを削除
rm -rf node_modules/.vite
# 方法 2:--force フラグで起動
yarn vite --force
# 方法 3:開発サーバー起動中に r を押す
# (ターミナルで r キーを押すと再起動)
トラブルシューティングのフローチャート
下図は、問題発生時の診断フローです。
mermaidflowchart TD
start["エラー発生"] --> check1{"import エラー?"}
check1 -->|Yes| solution1["include に追加"]
check1 -->|No| check2{"実行時エラー?"}
check2 -->|Yes| solution2["exclude に追加<br/>または esbuildOptions 調整"]
check2 -->|No| check3{"キャッシュ問題?"}
check3 -->|Yes| solution3["yarn vite --force"]
check3 -->|No| solution4["GitHub Issues を確認<br/>または報告"]
solution1 --> verify["動作確認"]
solution2 --> verify
solution3 --> verify
solution4 --> verify
実践例 4:パフォーマンス計測
optimizeDeps の効果を定量的に計測する方法を見ていきます。
計測用の設定
typescript// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
// デバッグログを有効化
logLevel: 'info',
optimizeDeps: {
include: ['react', 'react-dom', 'lodash-es'],
},
// ビルド情報を表示
build: {
reportCompressedSize: true,
},
});
起動時のログ確認
開発サーバー起動時に、以下のようなログが表示されます。
arduinovite v4.1.0 dev server running at:
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
ready in 324 ms.
Pre-bundling dependencies:
react
react-dom
lodash-es
(this will be run only when your dependencies change)
ログから読み取れる情報
項目 | 値 | 意味 |
---|---|---|
ready in 324 ms | 324 ミリ秒 | サーバー起動時間 |
Pre-bundling dependencies | react, react-dom, lodash-es | プリバンドル対象 |
ブラウザ DevTools での確認
Chrome DevTools の Network タブで、リクエスト数と読み込み時間を確認します。
最適化前
markdownName Size Time
---------------------------------------------
lodash-es/map.js 2.1 KB 15 ms
lodash-es/filter.js 1.8 KB 12 ms
lodash-es/reduce.js 2.3 KB 14 ms
... (600+ files)
---------------------------------------------
Total 450 KB 3.2 s
最適化後
markdownName Size Time
---------------------------------------------
lodash-es.js (prebundled) 180 KB 85 ms
react.js (prebundled) 120 KB 62 ms
react-dom.js (prebundled) 220 KB 95 ms
---------------------------------------------
Total 520 KB 1.1 s
最適化により、ファイル数は大幅に削減され、読み込み時間も 3 分の 1 になりました。
まとめ
本記事では、Vite の optimizeDeps と esbuild プリバンドル の仕組みを深掘りしてきました。重要なポイントを振り返りましょう。
核心となる仕組み
-
プリバンドルの 2 つの目的
- CommonJS → ESM への変換でブラウザ互換性を確保
- 多数のモジュールを結合して HTTP リクエストを削減
-
esbuild の圧倒的な速度
- Go 言語実装による 10〜100 倍の高速化
- 開発サーバー起動時間を数秒から数百ミリ秒に短縮
-
自動検出とカスタマイズ
- 通常は自動検出で十分に機能
- 動的 import や特殊なケースでは
include
/exclude
で調整
実務で活かすポイント
基本設定は不要
Vite は賢く依存関係を検出するため、ほとんどのケースで設定なしで最適化されます。
カスタマイズが必要なケース
- 動的 import を多用するプロジェクト →
include
で明示 - Monorepo 環境 → ワークスペースパッケージを
exclude
- 特殊な export 構造のパッケージ →
exclude
またはesbuildOptions
で調整
トラブル時の対処法
- キャッシュクリア:
yarn vite --force
- 依存関係の明示:
include
に追加 - プリバンドルの除外:
exclude
に追加
開発体験の向上
optimizeDeps を理解することで、以下のメリットが得られます。
- 爆速の開発サーバー起動:数秒が数百ミリ秒に
- 快適なホットリロード:変更が即座に反映
- 大規模プロジェクトでも高速:依存関係が増えても安定したパフォーマンス
Vite の内部動作を理解すれば、より効率的な開発環境を構築できるでしょう。プリバンドルは Vite の高速性を支える重要な仕組みであり、その中心には esbuild の驚異的な性能があります。
これらの知識を活かして、快適な開発ライフを送っていただければ幸いです。
関連リンク
- article
esbuild プリバンドルを理解する:Vite の optimizeDeps 深掘り
- article
Vite 本番の可観測性:ソースマップアップロードと Sentry 連携でエラーを特定
- article
Micro Frontends 設計:`vite-plugin-federation` で分割可能な UI を構築
- article
【保存版】Vite 設定オプション早見表:`resolve` / `optimizeDeps` / `build` / `server`
- article
Vite で始める Preact:公式プラグイン設定と最短プロジェクト作成【完全手順】
- article
ブラウザランナー vs Node ランナー:Vitest 実行環境の技術比較と選定基準
- article
NestJS 監視運用:SLI/SLO とダッシュボード設計(Prometheus/Grafana/Loki)
- article
WebRTC AV1/VP9/H.264 ベンチ比較 2025:画質・CPU/GPU 負荷・互換性を実測
- article
MySQL アラート設計としきい値:レイテンシ・エラー率・レプリカ遅延の基準
- article
Vitest フレーク検知技術の運用:`--retry` / シード固定 / ランダム順序で堅牢化
- article
Motion(旧 Framer Motion)デザインレビュー運用:Figma パラメータ同期と差分共有のワークフロー
- article
esbuild プリバンドルを理解する:Vite の optimizeDeps 深掘り
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来