こんにちは。テックリードの私だ。
日々のフロントエンド開発において、「`npm run build` の出力サイズが肥大化しすぎてCold Startに耐えられない」「初回ロード時に不要な重厚長大ライブラリまで丸ごとフェッチされている」「`node_modules` の闇がそのまま本番環境にデプロイされている」といった悪夢にうなされていないだろうか。
Viteはその爆速なDevサーバー(esbuildによるオンデマンドコンパイル)の恩恵で広く普及したが、本番ビルドの裏側ではRollupが厳格なモジュールグラフの結合を行っている。このRollupの挙動をデフォルトのまま放置することは、スポーツカーに軽自動車のエンジンを積むようなものだ。
今回は、Viteの `build.rollupOptions` を極限までハックし、バンドルサイズを削ぎ落とし、キャッシュ効率を最大化する「高度なチャンク分割戦略」と「外部化(External)」の実践知を伝授する。
—
1. なぜデフォルトのViteビルドでは戦えないのか?
Viteの初期設定で `build` を実行すると、エントリーポイントから依存関係を辿り、暗黙的にチャンク(出力ファイル)が生成される。しかし、中規模以上のプロダクトにおいて、このデフォルト挙動には致命的な弱点がある。
1. Vendor肥大化問題: `node_modules` からインポートした重量級ライブラリ(例: React, Lodash, UIコンポーネント群)が、アプリケーションコードと同一のチャンクに巻き込まれるか、あるいは雑に一つの `vendor.js` にまとめられる。その結果、わずか数行のビジネスロジックを修正しただけでも、数メガバイトの巨大なVendorキャッシュが無効化される。
2. CDN活用の欠如: 重厚なサードパーティライブラリ(React, ReactDOMなど)を自前でバンドルせず、信頼性の高いCDN(jsDelivrやunpkgなど)からES Modulesとして直読みさせたい場合、Viteのデフォルト機能だけでは対応できない。
これを解決するのが、Viteの内部でRollupを直接制御する `build.rollupOptions` である。
—
2. 外部化(External)による究極のバンドルダイエット
まずは、特定のライブラリをバンドルから除外し、外部(CDNまたはグローバル変数)として読み込ませる手法を解説する。これにより、ビルド時間の短縮とクライアント側のキャッシュ効率が劇的に跳ね上がる。
実践的な `vite.config.ts` の構築
以下の設定では、ReactとReactDOMをバンドルから完全に切り離し、ブラウザのグローバル変数(`window.React` 等)を参照するようにRollupへ指示している。
// vite.config.ts
import { defineConfig } from ‘vite’
import react from ‘@vitejs/plugin-react’
import { visualizer } from ‘rollup-plugin-visualizer’
export default defineConfig({
plugins: [
react(),
// 【神プラグイン】バンドル内の依存関係を視覚的なHTMLレポートとして出力する
// どのモジュールが容量を圧迫しているかを一目で暴き出す必須ツール
visualizer({
filename: ‘./dist/stats.html’,
open: true,
gzipSize: true,
brotliSize: true,
}),
],
build: {
// 20kB以下のインラインアセット化を無効化し、HTTPキャッシュの粒度を最適化
assetsInlineLimit: 0,
rollupOptions: {
// 1. 外部化(External)の定義
// ここに指定したパッケージ名は、バンドルにコンパイルされず、そのままインポート文として残る
external: [‘react’, ‘react-dom’],
output: {
// 2. 外部化したグローバル変数とのマッピング
// グローバルスコープに存在する変数名とES Modulesのインポート名を紐付ける
globals: {
react: ‘React’,
‘react-dom’: ‘ReactDOM’,
},
},
},
},
})
HTML側でのCDNインジェクション設定
外部化したライブラリは、HTMLのエントリーポイント(`index.html`)で事前に読み込んでおく必要がある。
このアプローチにより、自社アプリケーションコードのビルド成果物は数キロバイト単位まで軽量化され、差分デプロイ時の転送量も最小限に抑えられる。
—
3. `output.manualChunks` による高度なコード分割戦略
次に、すべてのサードパーティライブラリがCDNにあるわけではないケースにおける「自前チャンク分割戦略」を解説する。
デフォルトの `manualChunks` は、単に `node_modules` 全体を一括りにしがちだが、これでは「特定のページでしか使わない重量級ライブラリ(例: Chart.jsやThree.js)」まで初期ロード時に強制される。
意図した通りにモジュールを切り離す関数型アプローチ
`output.manualChunks` に関数を渡すことで、モジュールのパス(`id`)を検査し、ミリ単位でチャンクの所属先を制御できる。
// vite.config.ts (抜粋)
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
// node_modules 内のモジュールを対象にする
if (id.includes(‘node_modules’)) {
// Reactエコシステムは一つのチャンクに固めてキャッシュヒット率を上げる
if (id.includes(‘react’) || id.includes(‘react-dom’) || id.includes(‘scheduler’)) {
return ‘vendor-react’
}
// UIコンポーネントライブラリ(例: @mui や antd)は巨大なので独立させる
if (id.includes(‘@mui’) || id.includes(‘@emotion’)) {
return ‘vendor-ui’
}
// その他すべてのサードパーティライブラリ
return ‘vendor-libs’
}
},
},
},
},
})
この関数型 `manualChunks` を導入すると、ビルドログの出力結果が以下のように美しく整理される。
vite v5.x.x building for production…
transforming…
✓ 45 modules transformed.
dist/index.html治まり 0.42 kB │ gzip: 0.29 kB
dist/assets/vendor-react-B28f.js 139.42 kB │ gzip: 44.12 kB
dist/assets/vendor-ui-C91a.js 210.15 kB │ gzip: 62.80 kB
dist/assets/vendor-libs-D73x.js 45.20 kB │ gzip: 15.11 kB
dist/assets/index-F48b.js 12.50 kB │ gzip: 4.10 kB
ユーザーがアプリケーションのビジネスロジック(`index-F48b.js`)をどれだけ書き換えても、重い `vendor-react` や `vendor-ui` のハッシュ値は変動しないため、ブラウザキャッシュが強力に効き続ける。
—
4. チーム開発で絶対に守るべき設定共有化ルールとベストプラクティス
これほど強力な設定も、チームメンバーのローカル環境やCI/CDパイプラインで一貫して適用されなければ意味がない。大組織・複数チームでの開発において破綻しないためのガバナンスルールを提示する。
1. 設定のモジュール分離(Config Composition)
設定ファイルが肥大化すると可読性が落ちるため、ビルド設定は独立したファイルに切り出すべきだ。
.
├── config/
│ └── rollup.chunks.ts # チャンク分割ロジックの定義
├── vite.config.ts # メイン設定
└── package.json
2. 環境変数によるCDN切り替えの制御
開発環境(Local)ではCDNからの読み込みを行わず、ローカルの `node_modules` を使い、本番ビルド(Production)でのみ `external` を有効化する動的スイッチングを実装する。
// vite.config.ts
import { defineConfig, ConfigEnv } from ‘vite’
export default defineConfig(({ mode }: ConfigEnv) => {
const isProduction = mode === ‘production’
return {
build: {
rollupOptions: {
// 本番環境のみReactを外部化し、開発時は通常通りバンドルしてオフライン開発を担保
external: isProduction ? [‘react’, ‘react-dom’] : [],
output: {
globals: isProduction ? { react: ‘React’, ‘react-dom’: ‘ReactDOM’ } : {},
manualChunks(id) {
if (id.includes(‘node_modules’)) {
return ‘vendor’
}
}
}
}
}
}
})
—
5. 現場で即座に開発効率を引き上げる隠しコマンド・ショートカット
最後に、Vite + Rollup環境での開発スピードを極限までブーストするための実践知を共有しよう。
- 型安全な設定補完の強制:
VSCodeを使用している場合、設定ファイルの先頭にJSDocコメントを記述するか、`defineConfig` を利用することで、Rollupの複雑な型定義を完全にIDEに推論させ、タイポをゼロにできる。
- チャンク視覚化のCI組み込み:
先ほど導入した `rollup-plugin-visualizer` をCI(GitHub Actions等)のビルドステップに組み込み、前回のビルドサイズと比較して指定容量(例: +50KB以上)を超えた場合にPRを自動でブロックする仕組みを構築せよ。アーキテクチャの劣化を人間ではなくシステムが防ぐのだ。
—
結び
Viteの背後にあるRollupの挙動を支配することは、単なる「ビルドの最適化」に留まらない。ネットワーク帯域の節約、サーバーコストの削減、そしてユーザー体験(UX)の劇的な向上という、ビジネスインパクトに直結する極めて重要なエンジニアリングだ。
デフォルトの心地よさに甘んじるな。設定ファイルを自身の意のままに操り、プロダクトのパフォーマンスを極限まで研ぎ澄ませてほしい。