Vite

Vite 完全ガイド:次世代フロントエンドビルドツールの全貌

1. はじめに - Viteとは

Vite(フランス語で「速い」を意味し、発音は /viːt/)は、Vue.jsの生みの親であるEvan Youが開発した次世代フロントエンドビルドツールである。従来のwebpackやParcelといったバンドラーが抱えていた「開発サーバーの起動が遅い」「HMR(Hot Module Replacement)の反映に時間がかかる」という根本的な問題を、ブラウザのネイティブESモジュール機能を活用することで解決した。

なぜViteが生まれたのか

従来のビルドツールでは、開発サーバーを起動する際にプロジェクト全体をバンドルする必要があった。プロジェクトが大規模化するにつれ、以下の問題が顕著になっていた。

  • 開発サーバーの起動に数十秒から数分かかる
  • ファイル変更時のHMR反映が遅延する
  • 開発体験(DX)が著しく低下する

Viteはこれらの課題に対し、根本的に異なるアプローチを採用した。開発時にはバンドルを行わず、ブラウザのネイティブESモジュール機能を利用してソースコードをそのまま配信する。これにより、プロジェクトの規模に関わらず高速な開発サーバーの起動とHMRを実現している。

Viteの主な特徴

  • 瞬時のサーバー起動 - バンドル不要のネイティブESM活用
  • 高速なHMR - 50ms以下でのモジュール更新
  • 豊富なビルトイン機能 - TypeScript、CSS Modules、JSONインポート等
  • フレームワーク非依存 - React、Vue、Svelte、Preact等に対応
  • Rollup互換プラグイン - 豊富なエコシステムを活用可能
  • 本番ビルド最適化 - Rolldownによる高速バンドル
# Viteプロジェクトの作成
npm create vite@latest my-app -- --template react-ts

# ディレクトリに移動して依存関係をインストール
cd my-app
npm install

# 開発サーバーの起動
npm run dev

2. アーキテクチャ概要

Viteのアーキテクチャは、開発サーバービルドパイプラインという2つの主要コンポーネントで構成されている。それぞれが異なる最適化戦略を採用しており、開発時と本番環境で最高のパフォーマンスを発揮する。

2.1 開発サーバー(ネイティブESモジュール)

開発サーバーでは、ソースコードをバンドルせずにブラウザに配信する。ブラウザが import 文を検出すると、個別のHTTPリクエストとしてサーバーに要求を送り、Viteはオンデマンドで必要なモジュールのみを変換して返却する。

従来のバンドラー:
  ソースコード全体 → バンドル処理 → バンドルファイル → ブラウザ

Vite:
  ソースコード → ブラウザからの要求 → 個別モジュール変換 → ブラウザ

この「オンデマンド変換」方式により、プロジェクト内のモジュール数が増加しても起動速度に影響を与えない。

2.2 ビルドパイプライン(Rolldown)

本番ビルドでは、Rolldown(Rust製の高速バンドラー)を使用してコードをバンドルする。Rolldownは、Rollupとの互換性を保ちながらRustの高速性を活かした次世代バンドラーであり、以下の最適化を自動的に行う。

  • ツリーシェイキング - 未使用コードの除去
  • コード分割 - 動的インポートに基づくチャンク分割
  • アセット最適化 - 画像やフォントの最適化処理
  • CSS最適化 - Lightning CSSによる高速なCSS処理
本番ビルドフロー:
  ソースコード → Rolldown → ツリーシェイキング → コード分割 → 最適化 → 出力

2.3 依存関係と開発コードの区別

Viteは、プロジェクトのコードを**依存関係(Dependencies)ソースコード(Source Code)**に分類して処理する。

区分処理方法具体例
依存関係事前バンドル(Rolldown)node_modules 内のライブラリ
ソースコードオンデマンド変換(ESM).tsx, .vue, .svelte

依存関係は変更頻度が低いため起動時に一度だけ事前バンドルされ、以降はキャッシュが利用される。一方、ソースコードは頻繁に変更されるため、リクエスト時にオンデマンドで変換される。


3. 開発サーバー

Viteの開発サーバーは、従来のバンドラーとは根本的に異なるアプローチで動作する。ネイティブESモジュールを活用し、50ms以下のHMR更新を実現する。

3.1 index.htmlがエントリーポイント

Viteでは、index.html がアプリケーションのエントリーポイントとなる。これは従来のバンドラーがJavaScriptファイルをエントリーポイントとするのとは対照的である。

<!-- index.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>My Vite App</title>
</head>
<body>
  <div id="root"></div>
  <!-- type="module" によりブラウザがESモジュールとして処理 -->
  <script type="module" src="/src/main.tsx"></script>
</body>
</html>

<script type="module"> を使用することで、ブラウザはこのスクリプトをESモジュールとして認識し、import 文を個別のHTTPリクエストとして処理する。

3.2 ネイティブESMの仕組み

ブラウザがモジュールを要求する流れは以下のとおりである。

// src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './styles/global.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)

ブラウザはまず main.tsx を要求し、Viteがこれを変換して返す。その中に含まれる import 文を解析し、reactreact-dom/client./App./styles/global.css をそれぞれ個別にリクエストする。この連鎖的なリクエストにより、必要なモジュールのみが読み込まれる。

3.3 HMR(Hot Module Replacement)

ViteのHMRは、変更されたモジュールとその直接の依存関係のみを更新する。モジュールグラフ全体を再構築する必要がないため、プロジェクトの規模に関わらず一貫した速度を維持する。

// HMRが動作するコンポーネントの例
// src/components/Counter.tsx
import { useState } from 'react'

export function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div className="counter">
      <p>カウント: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>
        インクリメント
      </button>
    </div>
  )
}

// このファイルを保存すると、50ms以下でブラウザに反映される
// ステートも保持されたまま更新される(React Fast Refresh使用時)

3.4 開発サーバーの設定

// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  server: {
    // ポート番号
    port: 3000,
    // サーバー起動時にブラウザを自動で開く
    open: true,
    // ホスト指定(外部からのアクセスを許可)
    host: '0.0.0.0',
    // HTTPS設定
    https: false,
    // プロキシ設定(APIサーバーへの転送)
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
    // CORSの設定
    cors: true,
    // ファイル監視の設定
    watch: {
      usePolling: true, // Docker等の環境で必要な場合
    },
  },
})

4. 依存関係の事前バンドル

Viteは開発サーバーの初回起動時に、node_modules 内の依存関係を事前にバンドルする。このプロセスには2つの重要な目的がある。

4.1 CommonJS/UMDからESMへの変換

多くのnpmパッケージは、CommonJSやUMD形式で配布されている。ViteはこれらをネイティブESモジュール形式に変換し、ブラウザで直接利用可能にする。

// CommonJS形式のモジュール(変換前)
// node_modules/lodash-es は ESM だが、lodash 本体は CommonJS
const _ = require('lodash')
module.exports = _.debounce

// Viteによる変換後(ESM形式)
export { default } from '/node_modules/.vite/deps/lodash.js?v=abc123'

4.2 リクエスト数の最適化

大量の内部モジュールを持つパッケージ(例:lodash-esは600以上のモジュールを含む)をそのまま配信すると、ブラウザは数百のHTTPリクエストを発行する必要がある。事前バンドルにより、これらを単一のモジュールに統合する。

// lodash-es をそのまま使うと 600+ のHTTPリクエストが発生
import { debounce, throttle, cloneDeep } from 'lodash-es'

// Viteの事前バンドルにより、1つのリクエストに最適化される
// /node_modules/.vite/deps/lodash-es.js からすべてが提供される

4.3 キャッシュの仕組み

事前バンドルの結果は node_modules/.vite/deps ディレクトリにキャッシュされる。キャッシュは以下の条件で無効化される。

  • package.jsondependencies リストの変更
  • パッケージマネージャーのロックファイルの変更(package-lock.jsonyarn.lockpnpm-lock.yamlbun.lockb
  • vite.config.ts 内の関連フィールドの変更
# キャッシュを手動でクリアする場合
npx vite --force

# または node_modules/.vite を削除
rm -rf node_modules/.vite

4.4 事前バンドルのカスタマイズ

// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  optimizeDeps: {
    // 事前バンドルに含めるパッケージを追加
    include: ['linked-package', 'esm-package > cjs-dep'],
    // 事前バンドルから除外するパッケージ
    exclude: ['my-local-package'],
    // Rolldownのオプション
    rolldownOptions: {
      plugins: [/* カスタムプラグイン */],
    },
  },
})

4.5 Rolldownによる高速化

Viteの事前バンドルエンジンはRolldownを採用している。RolldownはRust製の高速バンドラーであり、大量の依存関係を持つプロジェクトでも高速にバンドルを完了する。これにより、開発サーバーの初回起動時間が大幅に短縮される。


5. HMR API

ViteはネイティブESモジュール上で動作するHMR APIを提供しており、import.meta.hot を通じてアクセスできる。フレームワーク固有のHMR統合は通常プラグインが処理するため、エンドユーザーが直接HMR APIを操作する必要はないが、カスタムロジックが必要な場合に利用できる。

5.1 基本的なHMR API

// src/utils/theme.ts
// HMRの条件ガード(本番ビルド時にはツリーシェイキングされる)
if (import.meta.hot) {
  // モジュールの更新を受け入れる(自己受容)
  import.meta.hot.accept((newModule) => {
    if (newModule) {
      // 新しいモジュールのエクスポートで更新処理
      console.log('テーマモジュールが更新されました')
      applyTheme(newModule.currentTheme)
    }
  })

  // モジュール破棄時のクリーンアップ
  import.meta.hot.dispose((data) => {
    // 副作用のクリーンアップ(タイマー、イベントリスナー等)
    clearInterval(data.timerId)
    console.log('古いモジュールが破棄されます')
  })

  // 次のHMR更新に状態を引き継ぐ
  import.meta.hot.data.timerId = setInterval(() => {
    checkThemeUpdates()
  }, 5000)
}

5.2 依存モジュールの更新を受け入れる

// src/app.ts
import { initRenderer } from './renderer'
import { loadConfig } from './config'

// 特定の依存モジュールの更新を受け入れる
if (import.meta.hot) {
  import.meta.hot.accept('./renderer', (newModule) => {
    // renderer.ts が更新された時の処理
    if (newModule) {
      newModule.initRenderer()
    }
  })

  // 複数モジュールの更新を一括で受け入れる
  import.meta.hot.accept(
    ['./renderer', './config'],
    ([newRenderer, newConfig]) => {
      // いずれかのモジュールが更新された時の処理
      if (newConfig) {
        loadConfig()
      }
      if (newRenderer) {
        newRenderer.initRenderer()
      }
    }
  )
}

5.3 HMRの無効化

// src/critical-module.ts
// このモジュールが変更された場合、フルリロードを行う
if (import.meta.hot) {
  import.meta.hot.invalidate('重要な変更のためフルリロードが必要です')
}

5.4 Vue/React/Preact統合

各フレームワークのHMR統合は、専用プラグインによって提供される。

// vite.config.ts - React
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [
    react(), // React Fast Refresh による HMR を自動設定
  ],
})
// vite.config.ts - Vue
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue(), // Vue SFC の HMR を自動設定
  ],
})
// vite.config.ts - Preact
import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'

export default defineConfig({
  plugins: [
    preact(), // Preact の Prefresh による HMR を自動設定
  ],
})

6. TypeScriptサポート

ViteはTypeScriptをビルトインでサポートしており、.ts および .tsx ファイルをそのままインポートできる。ViteはTypeScriptのトランスパイルのみを行い、型チェックは実行しない。型チェックはIDEやビルドプロセスで別途行うことが推奨される。

6.1 Oxc Transformer

ViteはデフォルトでOxc Transformerを使用してTypeScriptをJavaScriptに変換する。Oxcは、Rust製の高速なJavaScript/TypeScriptツールチェインであり、従来のesbuildと比較してさらに高速なトランスパイルを実現する。

// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  // TypeScriptトランスパイラーの選択
  // デフォルトは 'oxc'
  // 'esbuild' や 'swc'(実験的)も選択可能
  typescript: {
    transformer: 'oxc', // デフォルト
  },
})

6.2 isolatedModules制約

ViteのTypeScript処理はファイル単位のトランスパイルであるため、TypeScriptの isolatedModules に相当する制約がある。以下のパターンは使用できない。

// NG: const enum は isolatedModules で使用不可
// const enum Direction {
//   Up,
//   Down,
// }

// OK: 通常の enum を使用する
enum Direction {
  Up,
  Down,
}

// NG: 型の再エクスポートは明示的に行う
// export { SomeType } from './types'

// OK: type キーワードを使用する
export type { SomeType } from './types'

6.3 tsconfig.jsonの推奨設定

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2023", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "noEmit": true,
    "isolatedModules": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "verbatimModuleSyntax": true,
    "useDefineForClassFields": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}
// tsconfig.node.json(Vite設定ファイル用)
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2023"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "noEmit": true,
    "strict": true,
    "skipLibCheck": true,
    "allowImportingTsExtensions": true
  },
  "include": ["vite.config.ts"]
}

6.4 型チェックの実行

# tsc による型チェック(ビルド時)
npx tsc --noEmit

# vue-tsc を使用する場合(Vue プロジェクト)
npx vue-tsc --noEmit

# package.json のスクリプトに追加
# "scripts": {
#   "type-check": "tsc --noEmit",
#   "build": "tsc --noEmit && vite build"
# }

6.5 クライアント型定義

Viteは vite/client 型定義を提供しており、アセットインポートやHMR APIの型サポートが利用できる。

// src/vite-env.d.ts
/// <reference types="vite/client" />

// これにより以下の型が利用可能になる:
// - import.meta.env の型定義
// - import.meta.hot の型定義
// - .svg, .png 等のアセットインポートの型定義
// - .module.css の型定義

7. CSS処理

ViteはCSSの処理をビルトインでサポートしており、追加の設定なしに高度なCSS機能を利用できる。

7.1 CSSの @import とリベース

Viteは CSS の @import をインライン化する処理をサポートしている。url() 参照も自動的にリベースされるため、ファイルの配置場所を気にする必要がない。

/* src/styles/base.css */
@import './reset.css';
@import './variables.css';
@import './typography.css';

body {
  background-image: url('../assets/bg-pattern.png');
  /* パスは自動的にリベースされる */
}

7.2 CSSモジュール

.module.css で終わるファイルはCSSモジュールとして扱われ、対応するモジュールオブジェクトがエクスポートされる。

/* src/components/Button.module.css */
.button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.primary {
  background-color: #3b82f6;
  color: white;
}

.secondary {
  background-color: #6b7280;
  color: white;
}

.disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
// src/components/Button.tsx
import styles from './Button.module.css'

interface ButtonProps {
  variant?: 'primary' | 'secondary'
  disabled?: boolean
  children: React.ReactNode
}

export function Button({ variant = 'primary', disabled, children }: ButtonProps) {
  const className = [
    styles.button,
    styles[variant],
    disabled ? styles.disabled : '',
  ].filter(Boolean).join(' ')

  return (
    <button className={className} disabled={disabled}>
      {children}
    </button>
  )
}

7.3 CSSプリプロセッサ(Sass/Less/Stylus)

Viteは .scss.sass.less.styl.stylus ファイルのビルトインサポートを提供している。対応するプリプロセッサをインストールするだけで利用可能になる。

# Sass を使用する場合
npm add -D sass-embedded
# または
npm add -D sass

# Less を使用する場合
npm add -D less

# Stylus を使用する場合
npm add -D stylus
// src/styles/variables.scss
$primary-color: #3b82f6;
$secondary-color: #6b7280;
$border-radius: 8px;
$font-stack: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;

@mixin flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}

@mixin responsive($breakpoint) {
  @if $breakpoint == tablet {
    @media (min-width: 768px) { @content; }
  } @else if $breakpoint == desktop {
    @media (min-width: 1024px) { @content; }
  }
}
// src/components/Card.module.scss
@use '../styles/variables' as *;

.card {
  border: 1px solid #e5e7eb;
  border-radius: $border-radius;
  padding: 24px;
  background-color: white;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  transition: box-shadow 0.2s ease;

  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  }

  .title {
    font-family: $font-stack;
    font-size: 1.25rem;
    font-weight: 600;
    color: #111827;
    margin-bottom: 8px;
  }

  .content {
    color: #6b7280;
    line-height: 1.6;
  }

  @include responsive(tablet) {
    padding: 32px;
  }
}

7.4 Lightning CSS

ViteはLightning CSSをビルトインでサポートしている。Lightning CSSはRust製の高速CSSパーサー、トランスフォーマー、ミニファイアであり、CSSモジュール、CSSネスティング、カスタムメディアクエリなどを高速に処理する。

// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  css: {
    // Lightning CSS の設定
    lightningcss: {
      // ブラウザターゲットの指定
      targets: {
        chrome: 110,
        firefox: 115,
        safari: 16,
      },
      // CSSモジュールの設定
      cssModules: {
        pattern: '[name]_[local]_[hash:5]',
      },
      // ドラフト仕様の有効化
      drafts: {
        customMedia: true,
      },
    },
    // CSS モジュールの設定
    modules: {
      localsConvention: 'camelCase',
      scopeBehaviour: 'local',
      generateScopedName: '[name]__[local]___[hash:base64:5]',
    },
    // プリプロセッサのオプション
    preprocessorOptions: {
      scss: {
        additionalData: `@use "./src/styles/variables" as *;`,
        api: 'modern-compiler',
      },
      less: {
        math: 'always',
        globalVars: {
          primaryColor: '#3b82f6',
        },
      },
    },
  },
})

7.5 CSSコード分割

Viteは非同期チャンクで使用されるCSSを自動的に抽出し、個別のファイルとして生成する。CSSファイルは対応するチャンクが読み込まれる際に <link> タグで自動的にロードされ、CSSが読み込まれた後にのみチャンクが評価される。これにより、FOUC(Flash of Unstyled Content)を防止する。


8. 静的アセット

Viteは画像、フォント、動画などの静的アセットのインポートをビルトインでサポートしている。

8.1 アセットのインポート

// デフォルト: 解決済みURLとしてインポート
import logoUrl from './assets/logo.png'
// logoUrl は '/src/assets/logo.png'(開発時)
// またはハッシュ付きパス(本番時)

// ?url サフィックス: 明示的にURLとしてインポート
import workletUrl from './shader/worklet.js?url'

// ?raw サフィックス: 文字列としてインポート
import shaderSource from './shader/fragment.glsl?raw'

// ?worker サフィックス: Web Workerとしてインポート
import MyWorker from './worker.ts?worker'
// src/components/Logo.tsx
import logoSrc from '../assets/logo.svg'
import heroImage from '../assets/hero.webp'

export function Logo() {
  return (
    <div>
      <img src={logoSrc} alt="ロゴ" width={120} height={40} />
      <img src={heroImage} alt="ヒーロー画像" loading="lazy" />
    </div>
  )
}

8.2 publicディレクトリ

public ディレクトリ内のアセットは、変換されずにそのまま配信される。ルートパス / から直接アクセス可能である。

project/
├── public/
│   ├── favicon.ico       → /favicon.ico
│   ├── robots.txt        → /robots.txt
│   ├── og-image.png      → /og-image.png
│   └── fonts/
│       └── inter.woff2   → /fonts/inter.woff2
├── src/
│   └── ...
└── index.html
// public ディレクトリのファイルはルートパスで参照
export function Head() {
  return (
    <>
      <link rel="icon" href="/favicon.ico" />
      <meta property="og:image" content="/og-image.png" />
    </>
  )
}

注意: public 内のファイルはソースコードからインポートしてはならない。常に / からの絶対パスで参照する。


9. JSONとGlobインポート

9.1 JSONインポート

ViteはJSONファイルの直接インポートと名前付きインポートをサポートしている。

// src/data/config.json
{
  "appName": "My Vite App",
  "version": "1.0.0",
  "features": {
    "darkMode": true,
    "i18n": true
  }
}
// デフォルトインポート(オブジェクト全体)
import config from './data/config.json'
console.log(config.appName) // "My Vite App"

// 名前付きインポート(ツリーシェイキング可能)
import { appName, version } from './data/config.json'
console.log(appName) // "My Vite App"

9.2 Globインポート

import.meta.glob() を使用して、ファイルシステムからパターンマッチで複数のモジュールを一括インポートできる。

// 遅延読み込み(デフォルト)
const modules = import.meta.glob('./pages/**/*.tsx')
// 変換結果:
// {
//   './pages/Home.tsx': () => import('./pages/Home.tsx'),
//   './pages/About.tsx': () => import('./pages/About.tsx'),
//   './pages/Contact.tsx': () => import('./pages/Contact.tsx'),
// }

// 動的にモジュールを読み込む
for (const path in modules) {
  modules[path]().then((mod) => {
    console.log(path, mod)
  })
}
// eagerモード(即時読み込み)
const modules = import.meta.glob('./components/*.tsx', { eager: true })
// 変換結果:
// import * as __glob__0 from './components/Header.tsx'
// import * as __glob__1 from './components/Footer.tsx'
// const modules = {
//   './components/Header.tsx': __glob__0,
//   './components/Footer.tsx': __glob__1,
// }

// 名前付きエクスポートの指定
const setups = import.meta.glob('./modules/*.ts', {
  import: 'setup',
  eager: true,
})

// 文字列としてインポート
const markdowns = import.meta.glob('./docs/*.md', {
  query: '?raw',
  import: 'default',
})

// 複数パターンの指定
const files = import.meta.glob([
  './components/**/*.tsx',
  '!./components/**/*.test.tsx', // テストファイルは除外
])
// 実践例: ファイルベースルーティング
const pages = import.meta.glob('./pages/**/index.tsx')

const routes = Object.keys(pages).map((path) => {
  const routePath = path
    .replace('./pages', '')
    .replace('/index.tsx', '')
    .replace(/\[(\w+)\]/g, ':$1') || '/'

  return {
    path: routePath,
    component: pages[path],
  }
})

// 結果:
// [
//   { path: '/', component: () => import('./pages/index.tsx') },
//   { path: '/about', component: () => import('./pages/about/index.tsx') },
//   { path: '/users/:id', component: () => import('./pages/users/[id]/index.tsx') },
// ]

10. WebAssemblyとWeb Workers

10.1 WebAssembly

ViteはWebAssemblyモジュールのインポートをサポートしている。.wasm ファイルを直接インポートし、初期化関数として利用できる。

// WebAssembly モジュールのインポート
import init from './fibonacci.wasm?init'

// 初期化して使用
init().then((instance) => {
  const result = instance.exports.fibonacci(10)
  console.log(`フィボナッチ(10) = ${result}`)
})

// initにインポートオブジェクトを渡すことも可能
init({
  env: {
    log: (message: number) => console.log(`WASM: ${message}`),
  },
}).then((instance) => {
  instance.exports.run()
})

トップレベルawaitを使用した簡潔な書き方も可能である。

const instance = await init()
const result = instance.exports.calculate(42)

10.2 Web Workers

ViteはWeb Workerのインポートをビルトインでサポートしている。

// ?worker サフィックスでワーカーとしてインポート
import MyWorker from './heavy-computation.ts?worker'

const worker = new MyWorker()

worker.postMessage({ type: 'start', data: largeDataSet })

worker.addEventListener('message', (event) => {
  console.log('計算結果:', event.data)
})
// src/workers/heavy-computation.ts(ワーカーファイル)
self.addEventListener('message', (event) => {
  const { type, data } = event.data

  if (type === 'start') {
    // 重い計算処理
    const result = processData(data)
    self.postMessage({ type: 'result', data: result })
  }
})

function processData(data: number[]): number {
  return data.reduce((sum, val) => sum + Math.sqrt(val * val + 1), 0)
}
// ?sharedworker サフィックスで SharedWorker として使用
import SharedWorker from './shared.ts?sharedworker'

const sharedWorker = new SharedWorker()
sharedWorker.port.start()
sharedWorker.port.postMessage({ action: 'subscribe' })
// new Worker コンストラクタによるインポート
const worker = new Worker(
  new URL('./worker.ts', import.meta.url),
  { type: 'module' }
)

11. ビルド最適化

Viteの本番ビルドでは、複数の最適化が自動的に適用される。

11.1 CSSコード分割

非同期にインポートされるチャンクに関連するCSSは、自動的に個別のファイルとして抽出される。これにより、必要なCSSのみがロードされる。

// 動的インポートにより、関連するCSSも分割される
const AdminPanel = lazy(() => import('./pages/AdminPanel'))
// → admin-panel.[hash].js と admin-panel.[hash].css が生成される

11.2 プリロードディレクティブの自動生成

Viteは、ビルドされたHTMLに対して <link rel="modulepreload"> ディレクティブを自動的に注入する。これにより、直接インポートされるモジュールが事前に読み込まれ、ウォーターフォールを回避する。

11.3 非同期チャンクの最適化

実際のアプリケーションでは、非同期チャンクが共通モジュールをインポートすることが頻繁にある。Viteは自動的に共通モジュールを分割し、重複を排除する。

// vite.config.ts - ビルド最適化の設定
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    // ビルドターゲット
    target: 'es2022',
    // 出力ディレクトリ
    outDir: 'dist',
    // ソースマップの生成
    sourcemap: true,
    // チャンクサイズの警告閾値(KB)
    chunkSizeWarningLimit: 500,
    // Rolldownオプション
    rollupOptions: {
      output: {
        // 手動でのチャンク分割
        manualChunks: {
          vendor: ['react', 'react-dom'],
          utils: ['lodash-es', 'date-fns'],
        },
        // アセットファイル名のカスタマイズ
        assetFileNames: 'assets/[name]-[hash][extname]',
        chunkFileNames: 'js/[name]-[hash].js',
        entryFileNames: 'js/[name]-[hash].js',
      },
    },
    // CSS最小化にLightning CSSを使用
    cssMinify: 'lightningcss',
    // アセットのインライン化閾値(バイト)
    assetsInlineLimit: 4096,
  },
})

11.4 ライセンス生成

Viteは本番ビルド時にサードパーティライブラリのライセンス情報を自動的に収集し、ライセンスファイルを生成する。


12. プラグインシステム

ViteのプラグインシステムはRollupのプラグインインターフェースを拡張しており、Rollupプラグインとの互換性を保ちながらVite固有の機能も提供する。

12.1 プラグインの基本

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
import svgr from 'vite-plugin-svgr'

export default defineConfig({
  plugins: [
    react(),
    tsconfigPaths(),
    svgr(),
  ],
})

12.2 カスタムプラグインの作成

// plugins/vite-plugin-markdown.ts
import { Plugin } from 'vite'
import { marked } from 'marked'

export function markdownPlugin(): Plugin {
  return {
    name: 'vite-plugin-markdown',

    // モジュールの変換(Rollup互換フック)
    transform(code, id) {
      if (id.endsWith('.md')) {
        const html = marked(code)
        return {
          code: `export default ${JSON.stringify(html)}`,
          map: null,
        }
      }
    },

    // Vite固有フック: 開発サーバーの設定
    configureServer(server) {
      server.middlewares.use((req, res, next) => {
        if (req.url === '/api/health') {
          res.statusCode = 200
          res.end(JSON.stringify({ status: 'ok' }))
          return
        }
        next()
      })
    },

    // Vite固有フック: 設定の解決
    config(config, { command }) {
      if (command === 'build') {
        // ビルド時のみ適用する設定
        return {
          build: {
            sourcemap: true,
          },
        }
      }
    },

    // Vite固有フック: HTMLの変換
    transformIndexHtml(html) {
      return html.replace(
        '</head>',
        `<meta name="generator" content="Vite" />\n</head>`
      )
    },

    // Vite固有フック: HMRの処理
    handleHotUpdate({ file, server }) {
      if (file.endsWith('.md')) {
        console.log('Markdownファイルが更新されました:', file)
        // カスタムHMRイベントを送信
        server.ws.send({
          type: 'custom',
          event: 'md-update',
          data: { file },
        })
      }
    },
  }
}

12.3 公式プラグイン

プラグイン用途
@vitejs/plugin-reactReact Fast Refresh、JSX変換
@vitejs/plugin-vueVue SFCサポート
@vitejs/plugin-vue-jsxVue JSXサポート
@vitejs/plugin-legacyレガシーブラウザサポート

12.4 プラグインの適用順序

// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    {
      name: 'pre-plugin',
      enforce: 'pre', // Viteコアプラグインの前に実行
      transform(code, id) { /* ... */ },
    },
    {
      name: 'normal-plugin',
      // enforce なし: Viteコアプラグインの後に実行(デフォルト)
      transform(code, id) { /* ... */ },
    },
    {
      name: 'post-plugin',
      enforce: 'post', // Viteビルドプラグインの後に実行
      transform(code, id) { /* ... */ },
    },
  ],
})

12.5 条件付きプラグイン適用

// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    {
      name: 'dev-only-plugin',
      apply: 'serve', // 開発サーバー時のみ適用
      configureServer(server) {
        // 開発時のみのミドルウェア
      },
    },
    {
      name: 'build-only-plugin',
      apply: 'build', // ビルド時のみ適用
      generateBundle() {
        // ビルド時のみの処理
      },
    },
  ],
})

13. 環境API

Viteは開発環境と本番環境で異なる設定を適用するための仕組みを提供している。

13.1 環境変数

Viteは .env ファイルから環境変数を読み込み、import.meta.env を通じてクライアントコードに公開する。セキュリティのため、VITE_ プレフィックスが付いた変数のみがクライアントに公開される。

# .env(全環境共通)
VITE_APP_TITLE=My Vite App
VITE_API_URL=https://api.example.com

# .env.development(開発環境)
VITE_API_URL=http://localhost:8080/api
VITE_DEBUG=true

# .env.production(本番環境)
VITE_API_URL=https://api.production.com
VITE_SENTRY_DSN=https://xxx@sentry.io/123
// 環境変数の使用
console.log(import.meta.env.VITE_APP_TITLE)  // "My Vite App"
console.log(import.meta.env.VITE_API_URL)     // 環境に応じたURL
console.log(import.meta.env.MODE)             // "development" | "production"
console.log(import.meta.env.DEV)              // true(開発時)
console.log(import.meta.env.PROD)             // true(本番時)
console.log(import.meta.env.SSR)              // true(SSR時)
// 環境変数の型定義
// src/env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string
  readonly VITE_API_URL: string
  readonly VITE_DEBUG?: string
  readonly VITE_SENTRY_DSN?: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

13.2 ブラウザターゲット

// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    // モダンブラウザのみをターゲット(デフォルト)
    target: 'es2022',
    // 特定のブラウザバージョンを指定
    // target: ['chrome110', 'firefox115', 'safari16', 'edge110'],
  },
})

13.3 @vitejs/plugin-legacy

レガシーブラウザをサポートする必要がある場合は、@vitejs/plugin-legacy を使用する。

// vite.config.ts
import { defineConfig } from 'vite'
import legacy from '@vitejs/plugin-legacy'

export default defineConfig({
  plugins: [
    legacy({
      targets: ['defaults', 'not IE 11'],
      // ポリフィルの自動注入
      additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
    }),
  ],
})

14. SSR(サーバーサイドレンダリング)

ViteはSSRのための低レベルAPIを提供しており、フレームワークの作者がSSR機能を構築する基盤として利用できる。

14.1 基本的なSSRセットアップ

// server.ts - SSRサーバーの例
import express from 'express'
import { createServer as createViteServer } from 'vite'

async function createServer() {
  const app = express()

  // 開発モードではViteのミドルウェアを使用
  const vite = await createViteServer({
    server: { middlewareMode: true },
    appType: 'custom',
  })

  app.use(vite.middlewares)

  app.use('*', async (req, res) => {
    const url = req.originalUrl

    try {
      // index.html を読み込んでViteのHTML変換を適用
      let template = await vite.transformIndexHtml(
        url,
        '<html><body><div id="root"><!--ssr-outlet--></div></body></html>'
      )

      // SSR用エントリーモジュールをロード
      const { render } = await vite.ssrLoadModule('/src/entry-server.tsx')

      // アプリケーションをレンダリング
      const appHtml = await render(url)

      // テンプレートにHTMLを注入
      const html = template.replace('<!--ssr-outlet-->', appHtml)

      res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
    } catch (e) {
      vite.ssrFixStacktrace(e as Error)
      console.error(e)
      res.status(500).end((e as Error).message)
    }
  })

  app.listen(5173, () => {
    console.log('SSRサーバーが起動しました: http://localhost:5173')
  })
}

createServer()
// src/entry-server.tsx
import { renderToString } from 'react-dom/server'
import App from './App'

export function render(url: string) {
  return renderToString(<App url={url} />)
}

15. 設定ファイル

Viteの設定ファイルは vite.config.ts(または .js.mjs.mts)で定義する。プロジェクトルートに配置すると自動的に検出される。

15.1 包括的な設定例

// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig(({ command, mode }) => {
  // 環境変数をロード
  const env = loadEnv(mode, process.cwd(), '')

  return {
    // プラグイン
    plugins: [react()],

    // サーバー設定
    server: {
      port: 3000,
      open: true,
      proxy: {
        '/api': {
          target: env.API_SERVER || 'http://localhost:8080',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, ''),
        },
      },
    },

    // ビルド設定
    build: {
      target: 'es2022',
      outDir: 'dist',
      sourcemap: true,
      rollupOptions: {
        output: {
          manualChunks: {
            vendor: ['react', 'react-dom'],
          },
        },
      },
    },

    // CSS設定
    css: {
      modules: {
        localsConvention: 'camelCase',
      },
      preprocessorOptions: {
        scss: {
          additionalData: `@import "./src/styles/variables.scss";`,
        },
      },
    },

    // モジュール解決
    resolve: {
      alias: {
        '@': path.resolve(__dirname, './src'),
        '@components': path.resolve(__dirname, './src/components'),
        '@hooks': path.resolve(__dirname, './src/hooks'),
        '@utils': path.resolve(__dirname, './src/utils'),
        '@styles': path.resolve(__dirname, './src/styles'),
      },
    },

    // プレビューサーバー設定
    preview: {
      port: 4173,
    },

    // 依存関係の最適化
    optimizeDeps: {
      include: ['react', 'react-dom'],
    },
  }
})

15.2 主要な設定オプション一覧

オプション説明デフォルト値
rootプロジェクトルートprocess.cwd()
baseパブリックベースパス/
mode動作モードdevelopment / production
publicDir静的アセットディレクトリpublic
cacheDirキャッシュディレクトリnode_modules/.vite
server.port開発サーバーポート5173
build.outDirビルド出力先dist
build.targetビルドターゲットmodules
build.sourcemapソースマップ生成false

16. CSP(Content Security Policy)

ViteはCSPに対応するため、nonceベースのスクリプト読み込みをサポートしている。

16.1 Nonceサポート

// HTMLプラグインでnonceを注入する例
import { defineConfig } from 'vite'

export default defineConfig({
  html: {
    cspNonce: 'PLACEHOLDER', // プレースホルダー
  },
  plugins: [
    {
      name: 'csp-nonce',
      transformIndexHtml(html, ctx) {
        // サーバーサイドでnonceを動的に生成して置換
        const nonce = generateSecureNonce()
        return html.replace(/PLACEHOLDER/g, nonce)
      },
    },
  ],
})

SSR環境では、import.meta.hot やViteが注入するスクリプトタグに対して、自動的にnonce属性が付与される。html.cspNonce を設定すると、Viteが生成するすべてのスクリプトタグとスタイルタグにnonceが追加される。


17. システム要件

Viteを使用するには、以下のシステム要件を満たす必要がある。

17.1 Node.jsバージョン

  • Node.js 20.19+ または Node.js 22.12+ が必要
  • EOL(サポート終了)に達したNode.jsバージョンはサポートされない
# Node.jsバージョンの確認
node --version

# nvm を使用したバージョン管理
nvm install 22
nvm use 22

17.2 ブラウザサポート

開発サーバーでは、ネイティブESモジュールをサポートするモダンブラウザが必要である。

  • Chrome 87+
  • Firefox 78+
  • Safari 14+
  • Edge 88+

本番ビルドのデフォルトターゲットは、ネイティブESモジュール、ネイティブESM動的インポート、および import.meta をサポートするブラウザである。レガシーブラウザのサポートが必要な場合は @vitejs/plugin-legacy を使用する。


18. まとめ

Viteは、フロントエンド開発のビルドツールにおけるパラダイムシフトを実現した。本記事で解説した主要な特徴を振り返る。

Viteが選ばれる理由

  1. 圧倒的な速度 - ネイティブESMによる瞬時のサーバー起動と50ms以下のHMR
  2. ゼロコンフィグ - TypeScript、CSS Modules、JSON等がすぐに使える
  3. フレームワーク非依存 - React、Vue、Svelte、Preact等すべてに対応
  4. 豊富なプラグイン - Rollup互換プラグインエコシステムの活用
  5. 最適化されたビルド - Rolldownによる高速な本番ビルド
  6. SSRサポート - サーバーサイドレンダリングの基盤を提供
  7. 最新技術の採用 - Oxc、Rolldown、Lightning CSS等のRust製ツールチェイン

クイックスタートコマンド

# 各フレームワークのプロジェクトテンプレート
npm create vite@latest my-react-app -- --template react-ts
npm create vite@latest my-vue-app -- --template vue-ts
npm create vite@latest my-svelte-app -- --template svelte-ts
npm create vite@latest my-preact-app -- --template preact-ts
npm create vite@latest my-vanilla-app -- --template vanilla-ts

# 開発サーバーの起動
npm run dev

# 本番ビルド
npm run build

# ビルド結果のプレビュー
npm run preview

Viteは、Next.js、Nuxt、SvelteKit、Astro、Remix、Redwood等の多くのメタフレームワークでも採用されており、フロントエンドエコシステムにおける標準的なビルドツールとしての地位を確立している。

プロジェクトの規模や要件に関わらず、Viteは高速で効率的な開発体験を提供する。まだ導入していないプロジェクトがあれば、ぜひViteへの移行を検討してほしい。


参考リンク