vite-bundle-optimization について
本番ビルドの設定や、ビルドの遅さ・バンドル肥大化の調査に役立つ Vite + React 向けの最適化リファレンスです。インポート、コード分割、読み込み、アセット処理、Vite のビルド設定を扱います。
主な機能
- 01バレルインポートの回避
- 02手動チャンク分割の設定
- 03動的インポートによるルート分割
- 04画面下部コンポーネントの遅延読み込み
- 05サードパーティースクリプトの遅延
- 06バンドル内容の分析
活用シーン
- 01初期 JavaScript ペイロードの削減
- 02予想外に大きいバンドルの調査
- 03Vite 開発サーバーの起動改善
- 04本番ビルド設定のレビュー
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ patternsdev/ skills --skill vite-bundle-optimizationインストール後、Claude Code で/vite-bundle-optimizationを入力して読み込まれたか確認します。
詳細
Vite のバンドル最適化
目次
Vite + React アプリケーションのバンドルサイズとビルドパフォーマンスを最適化する、本番環境に対応したパターンです。Vite のアーキテクチャ(開発時はネイティブ ESM、本番環境では Rollup)を活用し、より小さく高速なバンドルを実現します。
使用する場面
次のような場合に、これらのパターンを参考にしてください。
- 本番環境向けに新しい Vite + React プロジェクトをセットアップするとき
npx vite-bundle-visualizerでバンドルサイズを分析するとき- ビルド時間が長い、またはバンドルが予想以上に大きいとき
- webpack/CRA から Vite に移行するとき
- Core Web Vitals(LCP、FID/INP、CLS)を最適化するとき
手順
- プロジェクトのセットアップ、ビルド設定、バンドルサイズのレビュー時に、これらのパターンを適用してください。バンドルが大きい場合やビルドが遅い場合は、
npx vite-bundle-visualizerで原因を調べ、該当するパターンを適用してください。
詳細
概要
Vite は、依存関係の事前バンドルと開発時の変換に esbuild を、本番ビルドに Rollup を使用します。この二重のアーキテクチャを理解することが、効果的な最適化の鍵です。以下のパターンは、効果の大きい順に並んでいます。
1. バレルファイルからのインポートを避ける
影響:重大 — 起動に 200~800ms、開発サーバーの起動に 2~4 秒余分にかかる場合があります。
バレルファイル(複数のモジュールから再エクスポートする index.ts)を使うと、エクスポートを 1 つしか使用しない場合でも、バンドラーはモジュールグラフ全体を読み込む必要があります。これは、React アプリのバンドルサイズに関する最大の問題です。
避ける — バレルファイル経由でライブラリ全体をインポート:
import { Button, TextField } from '@/components'
// Loads ALL components in the barrel, even unused ones
import { Check, X, Menu } from 'lucide-react'
// Loads all 1,500+ icons (~2.8s in dev)
推奨 — 直接インポート:
import { Button } from '@/components/Button'
import { TextField } from '@/components/TextField'
import Check from 'lucide-react/dist/esm/icons/check'
import X from 'lucide-react/dist/esm/icons/x'
import Menu from 'lucide-react/dist/esm/icons/menu'
vite-plugin-barrel で自動修正:
// vite.config.ts
import barrel from 'vite-plugin-barrel'
export default defineConfig({
plugins: [
react(),
barrel({
packages: ['lucide-react', '@mui/material', '@mui/icons-material'],
}),
],
})
これにより、ビルド時にバレルインポートが直接インポートに変換されるため、書きやすい構文と直接インポートのパフォーマンスを両立できます。
影響を受けやすいライブラリ: lucide-react、@mui/material、@mui/icons-material、@tabler/icons-react、react-icons、@radix-ui/react-*、lodash、date-fns、rxjs。
2. 手動チャンク分割を設定する
影響:大 — キャッシュの効率化、並列読み込み、初期バンドルの縮小につながります。
Vite のデフォルトのチャンク分割では、すべてのベンダーコードが 1 つのファイルにまとめられます。頻繁に変更されるアプリケーションコードによってベンダーキャッシュが無効化されないよう、分割してください。
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks: {
// Core React — rarely changes
'vendor-react': ['react', 'react-dom'],
// Router — changes infrequently
'vendor-router': ['react-router-dom'],
// Data layer — changes occasionally
'vendor-query': ['@tanstack/react-query'],
// UI framework — changes with design updates
'vendor-ui': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
},
},
},
},
})
モジュールパスに基づいて、より動的に分割する場合:
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('react-dom')) return 'vendor-react'
if (id.includes('react-router')) return 'vendor-router'
if (id.includes('@tanstack')) return 'vendor-query'
return 'vendor' // everything else
}
},
3. 動的インポートでルート単位にコードを分割する
影響:大 — 現在のページに必要なコードだけを読み込みます。
動的インポートと React.lazy() を使って、各ルートを個別のチャンクに分割します。
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
const Home = lazy(() => import('./pages/Home'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
)
}
Vite は、各遅延読み込みインポートに対して個別のチャンクを自動的に作成します。デバッグしやすいよう、名前を付けてください。
const Dashboard = lazy(() =>
import(/* webpackChunkName: "dashboard" */ './pages/Dashboard')
)
4. ファーストビューより下にある重いコンポーネントを遅延読み込みする
影響:大 — 初期バンドルを減らし、LCP を短縮します。
初期読み込み時に表示されないコンポーネント(モーダル、チャート、エディター、マップ)は、遅延読み込みしてください。
import { lazy, Suspense, useState } from 'react'
const RichTextEditor = lazy(() => import('./components/RichTextEditor'))
const ChartPanel = lazy(() => import('./components/ChartPanel'))
function ArticlePage() {
const [editing, setEditing] = useState(false)
return (
<article>
<h1>Article Title</h1>
<p>Content visible immediately...</p>
{editing && (
<Suspense fallback={<EditorSkeleton />}>
<RichTextEditor />
</Suspense>
)}
<Suspense fallback={<ChartSkeleton />}>
<ChartPanel />
</Suspense>
</article>
)
}
5. サードパーティ製スクリプトの読み込みを遅らせる
影響:大 — アナリティクス、トラッキング、ウィジェットによってレンダリングがブロックされないようにします。
重要度の低いサードパーティ製スクリプトは、ページの操作が可能になってから読み込みます。
避ける — 初期レンダリングをブロック:
// main.tsx
import * as Sentry from '@sentry/react'
import posthog from 'posthog-js'
Sentry.init({ dsn: '...' })
posthog.init('...')
推奨 — ハイドレーション/マウント後に読み込み:
// main.tsx — defer to idle time
function initThirdParty() {
import('@sentry/react').then(Sentry => {
Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN })
})
import('posthog-js').then(({ default: posthog }) => {
posthog.init(import.meta.env.VITE_POSTHOG_KEY)
})
}
if ('requestIdleCallback' in window) {
requestIdleCallback(initThirdParty)
} else {
setTimeout(initThirdParty, 2000)
}
外部スクリプトタグには、defer を使用するか、動的に挿入してください。
function loadScript(src: string) {
const script = document.createElement('script')
script.src = src
script.async = true
document.body.appendChild(script)
}
6. ユーザーの操作意図に応じて重要なアセットをプリロードする
影響:中 — ナビゲーション時に体感される遅延をなくします。
クリックを待つのではなく、ユーザーが操作する意図を示した時点(ホバー、フォーカス)で、ルートのコードの読み込みを開始します。
function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
const preload = () => {
// Vite creates a module preload for dynamic imports
switch (to) {
case '/dashboard':
import('./pages/Dashboard')
break
case '/settings':
import('./pages/Settings')
break
}
}
return (
<Link to={to} onMouseEnter={preload} onFocus={preload}>
{children}
</Link>
)
}
HTML の head 内で <link rel="modulepreload"> を使う場合:
<link rel="modulepreload" href="/assets/Home-abc123.js" />
Vite は、エントリーチャンクに対する <link rel="modulepreload"> を自動的に追加します。ユーザーが次に訪れると分かっているルートには、手動でプリロードを追加してください。
7. 依存関係の事前バンドルを設定する
影響:中 — 開発サーバーの起動とページの読み込みを高速化します。
Vite は esbuild を使って node_modules の依存関係を事前バンドルします。特殊なケースに対応できるよう設定してください。
// vite.config.ts
export default defineConfig({
optimizeDeps: {
// Force pre-bundle these (useful for CJS deps or deep imports)
include: [
'react',
'react-dom',
'react-router-dom',
'@tanstack/react-query',
'date-fns/format',
'date-fns/parseISO',
],
// Skip pre-bundling for these (already ESM, or causes issues)
exclude: ['@vite-pwa/assets-generator'],
},
})
開発時に多数の小さなリクエストが発生し、ページの読み込みが遅い場合は、通常、依存関係が事前バンドルされていないことが原因です。その依存関係を include に追加してください。
8. 圧縮を有効にする
影響:中 — 転送サイズを60~80%削減できます。
Viteではデフォルトで圧縮されません。本番環境用に圧縮プラグインを追加します。
// vite.config.ts
import viteCompression from 'vite-plugin-compression'
export default defineConfig({
plugins: [
react(),
viteCompression({ algorithm: 'gzip' }),
viteCompression({ algorithm: 'brotliCompress' }),
],
})
これにより、アセットと同じ場所に.gzファイルと.brファイルが生成されます。サーバー(Nginx、Cloudflare、Vercel)がそれらを配信するように設定してください。
9. バンドルを定期的に分析する
影響:情報提供 — サイズの増加をリリース前に検出できます。
依存関係に大きな変更を加えるたびに、バンドルビジュアライザーを実行します。
npx vite-bundle-visualizer
または、ビルドスクリプトに追加します。
{
"scripts": {
"build": "vite build",
"analyze": "vite build && npx vite-bundle-visualizer"
}
}
確認するポイント:
- gzip圧縮後に200KBを超える単一のチャンク — 分割を検討してください
- 複数のチャンクに読み込まれている重複ライブラリ
- 一部の関数しか使用していないのに、ライブラリ全体が読み込まれている
- 動的にインポートできる
node_modulesのコード
10. デッドコード除去にはimport.meta.envを使用する
影響:低~中 — 本番環境で未使用のコードパスを削除します。
Viteはビルド時にimport.meta.env.*を置き換えるため、Rollupでデッドコードをツリーシェイクできます。
// This code is completely removed in production
if (import.meta.env.DEV) {
console.log('Debug info:', data)
window.__DEBUG_DATA__ = data
}
// Feature flags eliminated at build time
if (import.meta.env.VITE_FEATURE_NEW_DASHBOARD === 'true') {
// Only included when flag is set
initNewDashboard()
}
カスタム環境変数は.envファイルで定義します。
# .env.production
VITE_FEATURE_NEW_DASHBOARD=true
VITE_API_URL=https://api.example.com
11. 画像と静的アセットを最適化する
影響:中 — 通常、画像は最もサイズの大きいアセットです。
Viteでアセットの処理を設定します。
// vite.config.ts
export default defineConfig({
build: {
assetsInlineLimit: 4096, // Inline assets < 4KB as base64
rollupOptions: {
output: {
assetFileNames: (assetInfo) => {
// Organize assets by type
if (/\.(png|jpe?g|gif|svg|webp|avif)$/.test(assetInfo.name ?? '')) {
return 'images/[name]-[hash][extname]'
}
if (/\.(woff2?|ttf|eot)$/.test(assetInfo.name ?? '')) {
return 'fonts/[name]-[hash][extname]'
}
return 'assets/[name]-[hash][extname]'
},
},
},
},
})
画像を自動的に圧縮するには、vite-plugin-image-optimizerを使用します。
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
export default defineConfig({
plugins: [
react(),
ViteImageOptimizer({
png: { quality: 80 },
jpeg: { quality: 80 },
webp: { quality: 80 },
}),
],
})
12. API開発用に開発サーバーのプロキシを設定する
影響:中 — CORSの問題を解消し、ローカル開発を簡素化できます。
ViteのSPAは通常、別のバックエンドAPIと通信します。server.proxyを設定して開発中のAPIリクエストを転送すると、CORSを回避でき、本番環境のURLパターンに合わせられます。
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
},
'/auth': {
target: 'http://localhost:3001',
changeOrigin: true,
},
// WebSocket support for real-time features
'/ws': {
target: 'ws://localhost:3001',
ws: true,
},
},
},
})
アプリのコードでは相対パス(fetch('/api/users'))を使用します。リクエストはViteの開発サーバーに送られ、そこからバックエンドにプロキシされます。本番環境では、リバースプロキシ(Nginx、Caddy)にも同じルーティングを設定してください。
13. vite-plugin-pwaでPWAサポートを追加する
影響:中 — ViteのSPAにオフライン機能、インストール可能性、アセットのキャッシュを追加できます。
オフラインサポートやインストール可能性が必要なSPAでは、vite-plugin-pwaを使ってサービスワーカーの生成、事前キャッシュ、マニフェストの作成を行えます。
// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.svg', 'robots.txt'],
manifest: {
name: 'My App',
short_name: 'App',
theme_color: '#ffffff',
icons: [
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
],
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\./,
handler: 'NetworkFirst',
options: { cacheName: 'api-cache', expiration: { maxEntries: 50 } },
},
],
},
}),
],
})
アプリをユーザーに通知せずに更新する場合は、registerType: 'autoUpdate'を使用します。ユーザーに更新通知を表示する場合は、registerType: 'prompt'を使用します。
14. CSSの方針を選ぶ
影響:中 — Viteは設定不要で複数のCSS方式をサポートします。
ViteはCSS Modules、PostCSS、プリプロセッサーをそのまま扱えます。ニーズに応じて選択してください。
CSS Modules — スコープ化されたスタイルで、実行時のコストがなく、Viteに組み込まれています。
// Button.module.css → automatically scoped
import styles from './Button.module.css'
function Button({ children }: { children: React.ReactNode }) {
return <button className={styles.primary}>{children}</button>
}
Tailwind CSS — ユーティリティファーストで、ViteのPostCSSサポートと連携します。
// vite.config.ts — no plugin needed, Tailwind uses PostCSS
// Just install tailwindcss and add postcss.config.js
// postcss.config.js
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}
CSS-in-JSに関する考慮事項: styled-componentsやEmotionなどのライブラリは、実行時のオーバーヘッドを追加します。パフォーマンスを重視するViteのSPAでは、CSS ModulesまたはTailwindを推奨します。CSS-in-JSが必要な場合は、Vanilla ExtractやPanda CSSなど、実行時処理のない選択肢を検討してください。
15. React CompilerをViteプラグインとして設定する
影響:高 — 自動メモ化により、手動でのuseMemo、useCallback、React.memoが不要になります。
React Compilerはコンポーネントを分析し、メモ化を自動的に挿入します。Viteプロジェクトでは、Babelプラグインとして追加します。
npm install -D babel-plugin-react-compiler
// vite.config.ts
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
react({
babel: {
plugins: ['babel-plugin-react-compiler'],
},
}),
],
})
有効にすると、手動でのuseMemo、useCallback、React.memoの呼び出しを段階的に削除できます。コンパイラーが自動的に処理します。有効化後にテストスイートを実行して、動作が維持されていることを確認してください。
コンパイラーにはReact 19が必要です。これはオプトイン方式であり、段階的に導入したい場合は'use memo'ディレクティブを使ってファイルごとに有効化できます。
参照元
patterns.devのパターン — Reactのウェブエンジニアリングコミュニティ全般に向けた、Vite固有の最適化ガイダンスです。