name: base-vite description: >- Vite(8+)の設定・プラグイン・HMR・環境変数・プロキシ・SSR・ライブラリモード・ 依存関係の事前バンドル・ビルド最適化を扱う時に使用する。 vite.config.ts や Vite プラグイン、Vite ベースのプロジェクトの作業時に有効化する。 user-invocable: false
Vite パターン
Vite 8+ プロジェクトのビルドツールと開発サーバーのパターン集。設定・環境変数・プロキシ・ ライブラリモード・依存関係の事前バンドル・プロダクションでよくある落とし穴を扱う。
適用タイミング
vite.config.ts/vite.config.jsの設定- 環境変数や
.envファイルのセットアップ - API バックエンドへの開発サーバープロキシの設定
- ビルド出力の最適化(チャンク・ミニファイ・アセット)
build.libによるライブラリの公開- 依存関係の事前バンドルや CJS/ESM 相互運用のトラブルシューティング
- HMR・開発サーバー・ビルドエラーのデバッグ
- Vite プラグインの選定や適用順序の決定
仕組み
- 開発モードはソースファイルをネイティブ ESM としてそのまま配信する — バンドルしない。 変換はモジュールリクエストごとにオンデマンドで行われるため、コールドスタートが速く HMR が正確。
- ビルドモードは Rolldown(v7+)または Rollup(v5–v6)でアプリをプロダクション用にバンドルする。 ツリーシェイキング・コード分割・Oxc ベースのミニファイを行う。
- 依存関係の事前バンドルは esbuild で CJS/UMD 依存を一度だけ ESM へ変換し、結果を
node_modules/.viteにキャッシュする。以降の起動ではこの処理をスキップする。 - プラグインは開発とビルドで統一されたインターフェースを共有する — 同じプラグインオブジェクトが、 開発サーバーのオンデマンド変換とプロダクションパイプラインの両方で動く。
- 環境変数はビルド時に静的にインライン化される。
VITE_プレフィックス付きの変数はバンドル内の 公開定数になり、プレフィックスなしの変数はクライアントコードから一切見えない。
設定
基本の設定
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
resolve: {
alias: { '@': new URL('./src', import.meta.url).pathname },
},
})
条件付きの設定
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd()) // VITE_ プレフィックスのみ(安全)
return {
plugins: [react()],
server: command === 'serve' ? { port: 3000 } : undefined,
define: {
__API_URL__: JSON.stringify(env.VITE_API_URL),
},
}
})
主要な設定オプション
| キー | デフォルト | 説明 |
|---|---|---|
root |
'.' |
プロジェクトルート(index.html の場所) |
base |
'/' |
デプロイ先アセットの公開ベースパス |
envPrefix |
'VITE_' |
クライアントへ公開する環境変数のプレフィックス |
build.outDir |
'dist' |
出力ディレクトリ |
build.minify |
'oxc' |
ミニファイア('oxc'・'terser'・false) |
build.sourcemap |
false |
true・'inline'・'hidden' |
プラグイン
定番プラグイン
プラグインのニーズの大半は、メンテナンスの行き届いた少数のパッケージでカバーできる。 自作する前にまずこれらを検討する。
| プラグイン | 目的 | 使うケース |
|---|---|---|
@vitejs/plugin-react-swc |
SWC による React HMR + Fast Refresh | React アプリのデフォルト(Babel 版より高速) |
@vitejs/plugin-react |
Babel による React HMR + Fast Refresh | Babel プラグイン(emotion・MobX デコレーター)が必要な場合のみ |
@vitejs/plugin-vue |
Vue 3 SFC サポート | Vue アプリ |
vite-plugin-checker |
tsc + ESLint をワーカースレッドで実行し HMR オーバーレイ表示 |
すべての TypeScript アプリ — vite build は型チェックしない |
vite-tsconfig-paths |
tsconfig.json の paths エイリアスを反映 |
tsconfig.json に既にエイリアスがある場合 |
vite-plugin-dts |
ライブラリモードで .d.ts を出力 |
TypeScript ライブラリの公開時 |
vite-plugin-svgr |
SVG を React コンポーネントとしてインポート | SVG をコンポーネントとして使う React アプリ |
rollup-plugin-visualizer |
バンドルのツリーマップ / サンバーストレポート | 定期的なバンドルサイズ監査(enforce: 'post' を使う) |
vite-plugin-pwa |
ゼロコンフィグ PWA + Workbox | オフライン対応アプリ |
重要: vite build はトランスパイルのみで型チェックをしない。vite-plugin-checker を追加するか
CI で tsc --noEmit を実行しない限り、型エラーは静かにプロダクションへ出荷される。
カスタムプラグインの作成
自作が必要になることはまれ — 大半のニーズは既存プラグインでカバーできる。必要な場合は
vite.config.ts 内のインラインから始め、再利用するときだけ切り出す。
// vite.config.ts — 最小のインラインプラグイン
function myPlugin(): Plugin {
return {
name: 'my-plugin', // 必須。一意にする
enforce: 'pre', // 'pre' | 'post'(任意)
apply: 'build', // 'build' | 'serve'(任意)
transform(code, id) {
if (!id.endsWith('.custom')) return
return { code: transformCustom(code), map: null }
},
}
}
主要なフック: transform(ソースの変換)、resolveId + load(仮想モジュール)、
transformIndexHtml(HTML への注入)、configureServer(開発用ミドルウェアの追加)、
hotUpdate(カスタム HMR — v7+ で非推奨の handleHotUpdate を置き換え)。
仮想モジュールは \0 プレフィックスの慣習を使う — resolveId が '\0virtual:my-id' を返すことで
他のプラグインが処理をスキップする。ユーザーコードは 'virtual:my-id' をインポートする。
プラグイン API の全容は vite.dev/guide/api-plugin を参照する。
変換パイプラインのデバッグには開発中に vite-plugin-inspect を使う。
HMR API
フレームワークプラグイン(@vitejs/plugin-react・@vitejs/plugin-vue 等)が HMR を自動処理する。
import.meta.hot を直接使うのは、更新をまたいで状態を保持する必要があるカスタム状態ストア・
開発ツール・フレームワーク非依存のユーティリティを作る場合のみとする。
// src/store.ts — バニラモジュールの手動 HMR
if (import.meta.hot) {
// 更新をまたいで状態を保持する(.data は再代入せず、必ずプロパティを変更する)
import.meta.hot.data.count = import.meta.hot.data.count ?? 0
// モジュールが置き換えられる前に副作用をクリーンアップする
import.meta.hot.dispose((data) => clearInterval(data.intervalId))
// 自身のモジュールの更新を受け入れる
import.meta.hot.accept()
}
import.meta.hot のコードはプロダクションビルドからツリーシェイキングで除去される —
ガードを手で外す必要はない。
環境変数
Vite は .env → .env.local → .env.[mode] → .env.[mode].local の順に読み込む
(後のものが前を上書きする)。*.local ファイルは gitignore され、ローカルのシークレット用。
クライアントサイドからのアクセス
クライアントコードへ公開されるのは VITE_ プレフィックス付きの変数のみ:
import.meta.env.VITE_API_URL // string
import.meta.env.MODE // 'development' | 'production' | カスタム
import.meta.env.BASE_URL // base 設定の値
import.meta.env.DEV // boolean
import.meta.env.PROD // boolean
import.meta.env.SSR // boolean
設定ファイル内での環境変数の利用
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd()) // VITE_ プレフィックスのみ(安全)
return {
define: {
__API_URL__: JSON.stringify(env.VITE_API_URL),
},
}
})
セキュリティ
VITE_ プレフィックスはセキュリティ境界ではない
VITE_ プレフィックス付きの変数はビルド時にクライアントバンドルへ静的にインライン化される。
ミニファイ・base64 エンコード・ソースマップの無効化では隠せない。攻撃者は配信された JavaScript から
あらゆる VITE_ 変数を抽出できる。
ルール: VITE_ 変数に入れてよいのは公開値(API URL・フィーチャーフラグ・公開鍵)のみ。
シークレット(API トークン・データベース URL・秘密鍵)は API やサーバーレス関数の背後の
サーバーサイドに置く。
loadEnv('') の罠
// Bad: 第 3 引数に '' を渡すと、サーバーのシークレットを含む全環境変数が読み込まれ、
// define 経由でクライアントコードへインライン化できる状態になる
const env = loadEnv(mode, process.cwd(), '')
// Good: プレフィックスを明示する
const env = loadEnv(mode, process.cwd(), ['VITE_', 'APP_'])
プロダクションのソースマップ
プロダクションのソースマップは元のソースコードを漏らす。エラートラッカー(Sentry・Bugsnag)へ アップロードしてローカルから削除する運用でない限り、無効のままにする。
build: {
sourcemap: false, // デフォルト — このままにする
}
.gitignore チェックリスト
.env.local・.env.*.local— ローカルのシークレット上書きdist/— ビルド出力node_modules/.vite— 事前バンドルキャッシュ(古いエントリが幻のエラーを引き起こす)
サーバープロキシ
// vite.config.ts — server.proxy
server: {
proxy: {
'/foo': 'http://localhost:4567', // 文字列の短縮形
'/api': {
target: 'http://localhost:8080',
changeOrigin: true, // バーチャルホストのバックエンドに必要
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
}
WebSocket をプロキシする場合はルート設定に ws: true を追加する。
ビルド最適化
手動チャンク
// vite.config.ts — build.rolldownOptions
build: {
rolldownOptions: {
output: {
// オブジェクト形式: 特定のパッケージをグループ化する
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-popover'],
},
},
},
}
// 関数形式: ヒューリスティックで分割する
manualChunks(id) {
if (id.includes('node_modules/react')) return 'react-vendor'
if (id.includes('node_modules')) return 'vendor'
}
パフォーマンス
バレルファイルを避ける
バレルファイル(ディレクトリ全体を再エクスポートする index.ts)は、1 つのシンボルを
インポートしただけでも、再エクスポートされた全ファイルの読み込みを Vite に強制する。
公式ドキュメントが挙げる開発サーバー低速化の最大要因。
// Bad — ユーティリティ 1 つのインポートでバレル全体が読み込まれる
import { slash } from '@/utils'
// Good — 直接インポートなら対象の 1 ファイルだけが読み込まれる
import { slash } from '@/utils/slash'
インポートの拡張子を明示する
暗黙の拡張子 1 つにつき、resolve.extensions 経由で最大 6 回のファイルシステムチェックが発生する。
大規模なコードベースでは積み重なる。
// Bad
import Component from './Component'
// Good
import Component from './Component.tsx'
tsconfig.json の allowImportingTsExtensions と resolve.extensions を、実際に使う拡張子だけに絞る。
ホットパスのルートをウォームアップする
server.warmup.clientFiles は、ブラウザがリクエストする前に既知のホットエントリを事前変換する —
大規模アプリのコールドロード時のリクエストウォーターフォールを解消する。
// vite.config.ts
server: {
warmup: {
clientFiles: ['./src/main.tsx', './src/routes/**/*.tsx'],
},
}
遅い開発サーバーのプロファイリング
vite dev が遅いと感じたら、まず vite --profile で起動し、アプリを操作してから p+enter で
.cpuprofile を保存する。Speedscope に読み込み、どのプラグインが
時間を食っているかを特定する — 多くの場合、コミュニティプラグインの buildStart・config・
configResolved フックが原因。
ライブラリモード
npm パッケージを公開する場合は build.lib を使う。設定の詳細よりも重要な footgun が 2 つある。
- 型は出力されない —
vite-plugin-dtsを追加するか、別途tsc --emitDeclarationOnlyを実行する。 - peer dependencies は必ず external 化する — 指定漏れの peer はライブラリへバンドルされ、 利用側でランタイム二重化エラーを引き起こす。
// vite.config.ts
build: {
lib: {
entry: 'src/index.ts',
formats: ['es', 'cjs'],
fileName: (format) => `my-lib.${format}.js`,
},
rolldownOptions: {
external: ['react', 'react-dom', 'react/jsx-runtime'], // すべての peer dep
},
}
SSR の externals
素の createServer({ middlewareMode: true }) セットアップはフレームワーク作者の領分。
ほとんどのアプリは Nuxt・Remix・SvelteKit・Astro・TanStack Start を使うべき。
フレームワーク利用者として調整するのは、依存が SSR で壊れたときの externals 設定:
// vite.config.ts — ssr オプション
ssr: {
external: ['node-native-package'], // SSR バンドル内で require() のまま残す
noExternal: ['esm-only-package'], // SSR 出力へ強制バンドル(SSR エラーの大半を解決)
target: 'node', // 'node' または 'webworker'
}
依存関係の事前バンドル
Vite は CJS/UMD を ESM へ変換しリクエスト数を減らすために、依存関係を事前バンドルする。
// vite.config.ts — optimizeDeps
optimizeDeps: {
include: [
'lodash-es', // 既知の重い依存を強制的に事前バンドルする
'cjs-package', // 相互運用の問題を起こす CJS 依存
'deep-lib/components/**', // 深いインポートには glob を使う
],
exclude: ['local-esm-package'], // exclude する場合は有効な ESM であること
force: true, // キャッシュを無視して再最適化(一時的なデバッグ用)
}
よくある落とし穴
開発とビルドの挙動が一致しない
開発時は esbuild/Rolldown による変換、ビルド時は Rolldown によるバンドルを使う。
CJS ライブラリは両者で挙動が異なることがある。デプロイ前に必ず vite build && vite preview で検証する。
デプロイ後の古いチャンク
新しいビルドは新しいチャンクハッシュを生成する。セッション継続中のユーザーが、 もう存在しない古いファイル名をリクエストする。Vite に組み込みの解決策はない。緩和策:
- デプロイ後も一定期間、古い
dist/assets/のファイルを配信し続ける - ルーターで動的インポートのエラーを捕捉し、ページを強制リロードする
Docker とコンテナ
Vite はデフォルトで localhost にバインドするため、コンテナの外から到達できない:
// vite.config.ts — Docker / コンテナ向け設定
server: {
host: true, // 0.0.0.0 にバインドする
hmr: { clientPort: 3000 }, // リバースプロキシの背後にいる場合
}
モノレポでのファイルアクセス
Vite はファイル配信をプロジェクトルート内に制限する。ルート外のパッケージはブロックされる:
// vite.config.ts — モノレポでのファイルアクセス
server: {
fs: {
allow: ['..'], // 親ディレクトリ(ワークスペースルート)を許可する
},
}
アンチパターン
// Bad: envPrefix を '' にすると、シークレットを含む全環境変数がクライアントへ公開される
envPrefix: ''
// Bad: アプリのソースコードで require() が動く前提 — Vite は ESM ファースト
const lib = require('some-lib') // import を使う
// Bad: node_modules を全部パッケージごとのチャンクに分割する — 数百の極小ファイルができる
manualChunks(id) {
if (id.includes('node_modules')) {
return id.split('node_modules/')[1].split('/')[0] // パッケージごとに 1 チャンク
}
}
// Bad: ライブラリモードで peer dep を external 化しない — ランタイム二重化エラーになる
// rolldownOptions.external なしの build.lib
// Bad: 非推奨の esbuild ミニファイアを使う
build: { minify: 'esbuild' } // 'oxc'(デフォルト)か 'terser' を使う
// Bad: import.meta.hot.data を再代入で書き換える
import.meta.hot.data = { count: 0 } // 誤り: 再代入ではなくプロパティを変更する
import.meta.hot.data.count = 0 // 正しい
プロセス面のアンチパターン:
vite previewをプロダクションサーバーとして使う — ビルド済みバンドルのスモークテスト用。dist/は本物の静的ホスト(NGINX・Cloudflare Pages・Vercel static)へデプロイするか、 マルチステージ Dockerfile を使う。vite buildが型チェックすると期待する — トランスパイルのみ。型エラーは静かに プロダクションへ出荷される。vite-plugin-checkerを追加するか CI でtsc --noEmitを実行する。- デフォルトで
@vitejs/plugin-legacyを入れる — バンドルが約 40% 肥大化し、ソースマップ系の バンドルアナライザーが壊れる。モダンブラウザの 95%+ のユーザーには不要。 推測ではなく実際のアナリティクスに基づいて判断する。 tsconfig.jsonの paths と重複するresolve.aliasを 30 個以上手書きする —vite-tsconfig-pathsを使う。Excalidraw や PostHog で観測された事例で、新規プロジェクトでは避ける。- 依存変更後に古い
node_modules/.viteを残す — 事前バンドルキャッシュが幻のエラーを起こす。 ブランチ切り替え時や依存へのパッチ適用後にクリアする。
クイックリファレンス
| パターン | 使うケース |
|---|---|
defineConfig |
常に使う — 型推論が効く |
loadEnv(mode, root, ['VITE_']) |
設定ファイル内での環境変数アクセス(プレフィックスを明示) |
vite-plugin-checker |
すべての TypeScript アプリ(型チェックの穴を埋める) |
vite-tsconfig-paths |
手書きの resolve.alias の代わりに使う |
optimizeDeps.include |
相互運用の問題を起こす CJS 依存 |
server.proxy |
開発時に API リクエストをバックエンドへルーティングする |
server.host: true |
Docker・コンテナ・リモートアクセス |
server.warmup.clientFiles |
ホットパスのルートを事前変換する |
build.lib + external |
npm パッケージの公開 |
manualChunks(オブジェクト形式) |
ベンダーバンドルの分割 |
vite --profile |
遅い開発サーバーのデバッグ |
vite build && vite preview |
プロダクションバンドルのローカル検証(プロダクションサーバーではない) |
関連
- スキル: base-react — React のパターン、 base-docker — コンテナ化された開発環境