T-CREATOR

Vite 大規模案件の `vite.config.ts` 設計:環境・役割別に設定を分割する

Vite 大規模案件の `vite.config.ts` 設計:環境・役割別に設定を分割する

大規模なフロントエンド開発では、ビルド設定が複雑化し、保守性が課題になることがあります。特に Vite を使ったプロジェクトでは、vite.config.ts に環境変数、プラグイン、パス解決、最適化設定などが集約され、気づけば数百行にも及ぶ巨大ファイルになってしまうケースも少なくありません。

本記事では、大規模案件における vite.config.ts の設計手法として、環境別・役割別に設定を分割する実践的なアプローチをご紹介します。設定の見通しを良くし、チーム開発での保守性を高めるための具体的な実装方法を、コード例とともに解説していきますね。

背景

Vite とビルド設定の役割

Vite は、高速な開発サーバーと最適化されたプロダクションビルドを提供する次世代フロントエンドビルドツールです。開発時には ESM(ES Modules)をネイティブに利用し、本番ビルドでは Rollup を使った効率的なバンドルを実現します。

vite.config.ts は、Vite の動作をカスタマイズするための中核となる設定ファイルで、以下のような役割を担います。

  • 開発サーバーの設定(ポート、プロキシ、HTTPS など)
  • ビルド最適化の設定(チャンク分割、圧縮、ソースマップなど)
  • プラグインの登録と設定
  • パスエイリアスやモジュール解決のカスタマイズ
  • 環境変数の管理

以下の図は、Vite が設定ファイルを読み込み、開発・本番の各フローでどのように活用されるかを示しています。

mermaidflowchart TB
  config["vite.config.ts"]
  dev["開発サーバー起動"]
  build["プロダクションビルド"]
  devServer["高速 HMR 対応<br/>開発サーバー"]
  bundle["最適化された<br/>本番バンドル"]

  config -->|設定読み込み| dev
  config -->|設定読み込み| build
  dev --> devServer
  build --> bundle

図で理解できる要点:

  • vite.config.ts は開発・本番の両方で参照される
  • 開発時は HMR(ホットモジュール置換)を活用した高速サーバーを起動
  • 本番ビルド時は Rollup ベースの最適化処理を実行

プロジェクトの成長と設定の肥大化

小規模なプロジェクトであれば、1 つの vite.config.ts ファイルで十分管理できます。しかし、プロジェクトが成長するにつれて、以下のような要素が追加されていきます。

typescript// 初期段階のシンプルな設定例
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

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

しかし、開発が進むと設定項目が増えていくのです。

typescript// 成長したプロジェクトの設定(一部抜粋)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import { visualizer } from 'rollup-plugin-visualizer';
// ... 他にも多数のプラグイン

多数のプラグイン導入により、インポート文だけでも数十行に及ぶこともあります。さらに環境ごとの設定、ビルド最適化、プロキシ設定などが加わると、ファイルは急速に肥大化していきます。

課題

単一ファイルによる保守性の低下

大規模プロジェクトで vite.config.ts を単一ファイルで管理すると、いくつかの課題が浮き彫りになります。

可読性の問題

設定ファイルが 300 行、500 行と膨らむと、どこに何が書かれているのか把握するのが困難になります。新しいメンバーがプロジェクトに参加した際、設定の全体像を理解するまでに時間がかかってしまうでしょう。

typescript// 肥大化した設定ファイルの例(構造のみ)
export default defineConfig({
  // プラグイン設定: 50行
  plugins: [
    /* ... */
  ],

  // ビルド設定: 80行
  build: {
    /* ... */
  },

  // サーバー設定: 40行
  server: {
    /* ... */
  },

  // 環境別の条件分岐: 60行
  // テスト環境用の設定: 50行
  // ... さらに続く
});

このような状態では、特定の設定を探すだけでもスクロールが必要になり、開発効率が低下してしまいます。

環境別設定の管理の複雑さ

開発環境、ステージング環境、本番環境では、それぞれ異なる設定が必要になるケースがあります。

  • 開発環境: ソースマップを詳細に、HMR を有効化、プロキシで API サーバーに接続
  • ステージング環境: 本番に近い設定だがデバッグ情報も残す
  • 本番環境: 最大限の最適化、圧縮、セキュリティ設定

単一ファイルで環境分岐を行うと、条件分岐が複雑になります。

typescript// 環境分岐が複雑化した例
export default defineConfig(({ mode }) => {
  if (mode === 'development') {
    return {
      // 開発用設定
    };
  } else if (mode === 'staging') {
    return {
      // ステージング用設定
    };
  } else if (mode === 'production') {
    return {
      // 本番用設定
    };
  }
  // デフォルト設定...
});

条件分岐が増えると、どの設定がどの環境で適用されるのか追跡が難しくなります。

機能ごとの責務が不明確

プラグイン設定、ビルド最適化、プロキシ設定、テスト設定など、異なる責務を持つ設定が 1 ファイルに混在すると、変更時の影響範囲が把握しにくくなるでしょう。

以下の図は、単一ファイルで管理する場合の課題を視覚化したものです。

mermaidflowchart TD
  config["vite.config.ts<br/>(500行以上)"]

  subgraph problems["課題"]
    p1["可読性低下"]
    p2["環境分岐が複雑"]
    p3["責務が不明確"]
    p4["変更影響の把握困難"]
  end

  config --> p1
  config --> p2
  config --> p3
  config --> p4

  p1 -.->|結果| team["チーム開発の<br/>生産性低下"]
  p2 -.->|結果| team
  p3 -.->|結果| team
  p4 -.->|結果| team

図で理解できる要点:

  • 肥大化した単一ファイルは複数の課題を抱える
  • 各課題が相互に影響し、チーム開発の生産性を低下させる
  • 構造的な改善が必要

変更時のリスクと影響範囲

単一ファイルで管理していると、1 つの設定変更が予期しない影響を及ぼすリスクも高まります。たとえば、開発環境のプロキシ設定を変更したつもりが、本番ビルドにも影響してしまうといったケースです。

また、Git でのコンフリクトも発生しやすくなります。複数の開発者が同じファイルを編集すると、マージ時にコンフリクトが頻発し、解決に時間を取られることになるでしょう。

解決策

設定分割の基本戦略

大規模案件の vite.config.ts を効果的に管理するには、以下の 3 つの軸で設定を分割する戦略が有効です。

1. 環境別の分割

開発、ステージング、本番など、実行環境ごとに設定ファイルを分離します。各環境で必要な設定だけを記述することで、条件分岐を削減できるのです。

2. 役割別の分割

プラグイン、ビルド最適化、サーバー設定など、機能や責務ごとに設定を分離します。これにより、特定の機能に関する変更時の影響範囲を限定できます。

3. 共通設定の抽出

すべての環境で共通する設定は別ファイルに抽出し、各環境の設定で読み込む形にします。DRY 原則(Don't Repeat Yourself)に従い、重複を避けましょう。

以下の図は、分割後の設定ファイル構成を示しています。

mermaidflowchart TB
  subgraph base["共通設定層"]
    baseConfig["config/base.ts<br/>(共通設定)"]
    plugins["config/plugins.ts<br/>(プラグイン)"]
    alias["config/alias.ts<br/>(パスエイリアス)"]
  end

  subgraph env["環境別設定層"]
    dev["config/dev.ts<br/>(開発環境)"]
    stg["config/staging.ts<br/>(ステージング)"]
    prod["config/production.ts<br/>(本番環境)"]
  end

  subgraph root["統合層"]
    main["vite.config.ts<br/>(メイン)"]
  end

  baseConfig --> main
  plugins --> main
  alias --> main
  dev --> main
  stg --> main
  prod --> main

  main -->|環境判定| final["最終設定"]

図で理解できる要点:

  • 設定を共通層と環境別層に分離
  • メインの vite.config.ts は統合の役割に専念
  • 各ファイルの責務が明確化

ディレクトリ構成の設計

実際のプロジェクトでは、以下のようなディレクトリ構成を推奨します。

csharpproject-root/
├── vite.config.ts          # メインの設定ファイル(統合用)
├── config/                 # 設定ファイルディレクトリ
│   ├── base.ts            # 全環境共通の基本設定
│   ├── plugins.ts         # プラグイン設定
│   ├── alias.ts           # パスエイリアス設定
│   ├── build.ts           # ビルド最適化設定
│   ├── environments/      # 環境別設定
│   │   ├── development.ts
│   │   ├── staging.ts
│   │   └── production.ts
│   └── types.ts           # 設定用の型定義

このようにファイルを分割することで、目的のファイルをすぐに見つけられますし、変更時の影響範囲も明確になります。

設定マージの手法

分割した設定ファイルを統合するには、Vite が提供する mergeConfig ユーティリティを使用します。

typescript// マージの基本パターン
import { mergeConfig } from 'vite';
import type { UserConfig } from 'vite';

mergeConfig 関数は、2 つの設定オブジェクトをディープマージして統合してくれます。配列は結合され、オブジェクトは再帰的にマージされるため、複数の設定ファイルを柔軟に組み合わせられるのです。

typescript// マージの実装例
const baseConfig: UserConfig = {
  /* 基本設定 */
};
const envConfig: UserConfig = {
  /* 環境固有設定 */
};

export default mergeConfig(baseConfig, envConfig);

後から指定した設定が優先されるため、環境固有の設定で共通設定を上書きすることも可能です。

環境判定の実装

実行環境を判定するには、Vite の設定関数に渡される mode パラメータと、Node.js の環境変数を組み合わせます。

typescript// 環境判定のヘルパー関数例
export const isDevelopment = (mode: string) =>
  mode === 'development';
export const isProduction = (mode: string) =>
  mode === 'production';
export const isStaging = (mode: string) =>
  mode === 'staging';

これらのヘルパー関数を使うことで、環境判定のロジックを一箇所に集約できます。

具体例

ファイル構成の実装

実際のプロジェクトでの実装例を、段階的にご紹介していきます。まずは設定ファイルの型定義から見ていきましょう。

型定義ファイル

設定ファイル全体で共通して使う型を定義します。

typescript// config/types.ts
import type { UserConfig } from 'vite';

// 環境の型定義
export type Environment =
  | 'development'
  | 'staging'
  | 'production';

// 設定ファイルのエクスポート型
export type ConfigExport =
  | UserConfig
  | ((env: ConfigEnv) => UserConfig);

// 環境情報の型
export interface ConfigEnv {
  mode: Environment;
  command: 'build' | 'serve';
}

型定義を別ファイルに切り出すことで、各設定ファイルで型を再利用でき、タイプセーフな設定管理が実現できます。

パスエイリアス設定

パスエイリアスは全環境で共通なので、専用ファイルに分離します。

typescript// config/alias.ts
import path from 'path';
import type { AliasOptions } from 'vite';

// プロジェクトルートからの相対パス解決
const resolvePath = (relativePath: string) =>
  path.resolve(__dirname, '..', relativePath);

パス解決のユーティリティ関数を定義することで、各エイリアスの定義が簡潔になります。

typescript// config/alias.ts(続き)
export const alias: AliasOptions = {
  '@': resolvePath('src'),
  '@components': resolvePath('src/components'),
  '@utils': resolvePath('src/utils'),
  '@hooks': resolvePath('src/hooks'),
  '@types': resolvePath('src/types'),
  '@assets': resolvePath('src/assets'),
  '@api': resolvePath('src/api'),
  '@store': resolvePath('src/store'),
};

アプリケーションの規模に応じて、必要なエイリアスを追加していけばよいでしょう。

プラグイン設定

プラグインの設定も別ファイルに切り出します。プラグインは数が多くなりがちなので、グループ化して管理するとわかりやすくなります。

typescript// config/plugins.ts
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import { visualizer } from 'rollup-plugin-visualizer';
import type { PluginOption } from 'vite';

まず、必要なプラグインをインポートします。

typescript// config/plugins.ts(続き)
// 基本プラグイン(全環境共通)
export const basePlugins: PluginOption[] = [
  // React のサポート
  react({
    // Fast Refresh の設定
    fastRefresh: true,
    // Babel プラグインの設定
    babel: {
      plugins: [
        // 開発時の便利機能を追加
      ],
    },
  }),
  // tsconfig.json のパス設定を Vite に反映
  tsconfigPaths(),
];

基本プラグインは全環境で使用するものを定義します。

typescript// config/plugins.ts(続き)
// 開発環境専用プラグイン
export const devPlugins: PluginOption[] = [
  // 開発時のデバッグ用プラグインなど
];

// 本番環境専用プラグイン
export const prodPlugins: PluginOption[] = [
  // バンドルサイズの可視化
  visualizer({
    filename: 'dist/stats.html',
    open: false,
    gzipSize: true,
    brotliSize: true,
  }),
];

環境ごとに必要なプラグインを分けて定義することで、不要なプラグインの読み込みを避けられます。

ビルド最適化設定

本番環境でのビルド最適化設定を専用ファイルにまとめます。

typescript// config/build.ts
import type { BuildOptions } from 'vite';

export const buildConfig: BuildOptions = {
  // 出力ディレクトリ
  outDir: 'dist',

  // ソースマップの生成(本番では無効化も検討)
  sourcemap: false,

  // ファイルサイズ警告の閾値(KB)
  chunkSizeWarningLimit: 1000,
};

基本的なビルド設定を定義します。

typescript// config/build.ts(続き)
// チャンク分割の詳細設定
export const rollupOptions: BuildOptions['rollupOptions'] =
  {
    output: {
      // チャンク分割戦略
      manualChunks: {
        // React 関連を別チャンクに
        'react-vendor': [
          'react',
          'react-dom',
          'react-router-dom',
        ],

        // UI ライブラリを別チャンクに
        'ui-vendor': ['@mui/material', '@emotion/react'],

        // ユーティリティライブラリを別チャンクに
        'utils-vendor': ['lodash-es', 'date-fns', 'axios'],
      },

      // ファイル名のパターン
      chunkFileNames: 'assets/js/[name]-[hash].js',
      entryFileNames: 'assets/js/[name]-[hash].js',
      assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
    },
  };

チャンク分割を適切に設定することで、初回ロード時間を短縮し、キャッシュ効率も向上します。

基本設定

全環境で共通する基本設定をまとめます。

typescript// config/base.ts
import type { UserConfig } from 'vite';
import { alias } from './alias';
import { basePlugins } from './plugins';

export const baseConfig: UserConfig = {
  // パスエイリアス
  resolve: {
    alias,
  },

  // 基本プラグイン
  plugins: basePlugins,

  // CSS 設定
  css: {
    modules: {
      // CSS Modules の命名規則
      generateScopedName:
        '[name]__[local]__[hash:base64:5]',
    },
    preprocessorOptions: {
      scss: {
        // SCSS のグローバル変数をインポート
        additionalData:
          '@import "@/styles/variables.scss";',
      },
    },
  },
};

CSS の設定など、環境を問わず共通する項目をここに集約します。

開発環境設定

開発環境専用の設定を定義します。

typescript// config/environments/development.ts
import type { UserConfig } from 'vite';
import { devPlugins } from '../plugins';

export const developmentConfig: UserConfig = {
  // 開発環境専用プラグインを追加
  plugins: devPlugins,

  // 開発サーバー設定
  server: {
    port: 3000,
    host: true,
    open: true,

    // HMR 設定
    hmr: {
      overlay: true,
    },
  },
};

開発サーバーの基本設定を行います。

typescript// config/environments/development.ts(続き)
// プロキシ設定(続き)
export const developmentProxyConfig: UserConfig['server'] =
  {
    proxy: {
      // API リクエストをバックエンドに転送
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },

      // WebSocket 接続用のプロキシ
      '/ws': {
        target: 'ws://localhost:8000',
        ws: true,
      },
    },
  };

開発時に API サーバーとの連携が必要な場合は、プロキシ設定を追加します。

ステージング環境設定

ステージング環境は本番に近い設定にしつつ、デバッグ情報も残します。

typescript// config/environments/staging.ts
import type { UserConfig } from 'vite';
import { buildConfig, rollupOptions } from '../build';

export const stagingConfig: UserConfig = {
  // ビルド設定
  build: {
    ...buildConfig,
    // ステージングではソースマップを生成
    sourcemap: true,
    rollupOptions,
  },

  // 本番に近いが、デバッグ可能な状態を維持
  define: {
    'process.env.NODE_ENV': JSON.stringify('staging'),
    __APP_VERSION__: JSON.stringify(
      process.env.npm_package_version
    ),
  },
};

ステージング環境では、デバッグのためにソースマップを有効にしておくとよいでしょう。

本番環境設定

本番環境では最大限の最適化を施します。

typescript// config/environments/production.ts
import type { UserConfig } from 'vite';
import { buildConfig, rollupOptions } from '../build';
import { prodPlugins } from '../plugins';

export const productionConfig: UserConfig = {
  // 本番環境専用プラグイン
  plugins: prodPlugins,

  // 本番ビルド設定
  build: {
    ...buildConfig,
    // ソースマップは生成しない
    sourcemap: false,
    // minify を有効化
    minify: 'terser',
    // Terser のオプション
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },
    rollupOptions,
  },
};

本番環境では console.logdebugger を削除し、ファイルサイズを最小化します。

typescript// config/environments/production.ts(続き)
// 本番環境の環境変数設定
export const productionDefineConfig: UserConfig['define'] =
  {
    'process.env.NODE_ENV': JSON.stringify('production'),
    __APP_VERSION__: JSON.stringify(
      process.env.npm_package_version
    ),
    __BUILD_DATE__: JSON.stringify(
      new Date().toISOString()
    ),
  };

本番ビルドにバージョン情報やビルド日時を埋め込むことで、デプロイ後の追跡が容易になります。

メイン設定ファイル

最後に、すべての設定を統合するメインファイルを作成します。

typescript// vite.config.ts
import { defineConfig, mergeConfig } from 'vite';
import type { UserConfig } from 'vite';
import { baseConfig } from './config/base';
import { developmentConfig } from './config/environments/development';
import { stagingConfig } from './config/environments/staging';
import { productionConfig } from './config/environments/production';

必要な設定ファイルをすべてインポートします。

typescript// vite.config.ts(続き)
export default defineConfig(({ mode }) => {
  // 環境に応じた設定を選択
  let envConfig: UserConfig;

  switch (mode) {
    case 'development':
      envConfig = developmentConfig;
      break;
    case 'staging':
      envConfig = stagingConfig;
      break;
    case 'production':
      envConfig = productionConfig;
      break;
    default:
      // デフォルトは開発環境
      envConfig = developmentConfig;
  }

  // 基本設定と環境別設定をマージ
  return mergeConfig(baseConfig, envConfig);
});

メインファイルは環境判定と設定のマージに専念し、シンプルに保ちます。

設定の読み込みフロー

以下の図は、実行時に設定がどのように読み込まれ統合されるかを示しています。

mermaidsequenceDiagram
  participant CLI as Vite CLI
  participant Main as vite.config.ts
  participant Base as config/base.ts
  participant Env as config/environments/*.ts
  participant Merge as mergeConfig

  CLI->>Main: yarn dev --mode=development
  Main->>Main: mode='development' を取得
  Main->>Base: baseConfig を読み込み
  Main->>Env: developmentConfig を読み込み
  Main->>Merge: mergeConfig(base, env)
  Merge->>Merge: 設定をディープマージ
  Merge-->>Main: 統合された設定を返却
  Main-->>CLI: 最終設定を返却
  CLI->>CLI: 開発サーバーを起動

図で理解できる要点:

  • CLI から渡された mode パラメータで環境を判定
  • 基本設定と環境別設定を読み込み
  • mergeConfig で統合して最終設定を生成

実行コマンド

各環境で実行するコマンドを package.json に定義します。

json{
  "scripts": {
    "dev": "vite --mode development",
    "build:staging": "vite build --mode staging",
    "build:prod": "vite build --mode production",
    "preview": "vite preview"
  }
}

--mode オプションで環境を切り替えることで、対応する設定ファイルが読み込まれます。

環境変数の管理

環境ごとに異なる環境変数を管理するには、.env ファイルも分割します。

bash.env                 # 全環境共通のデフォルト値
.env.development     # 開発環境用
.env.staging         # ステージング環境用
.env.production      # 本番環境用
.env.local           # ローカル環境用(Git 管理外)

Vite は自動的に環境に応じた .env ファイルを読み込んでくれます。

bash# .env.development
VITE_API_BASE_URL=http://localhost:8000
VITE_ENABLE_MOCK=true

開発環境では API のベース URL をローカルに設定します。

bash# .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_ENABLE_MOCK=false

本番環境では実際の API エンドポイントを指定します。

バリデーションとエラーハンドリング

設定ファイルの読み込み時にバリデーションを追加すると、設定ミスを早期に発見できます。

typescript// config/validation.ts
import type { UserConfig } from 'vite';

// 必須項目のチェック
export const validateConfig = (
  config: UserConfig
): void => {
  // プラグインが設定されているかチェック
  if (!config.plugins || config.plugins.length === 0) {
    throw new Error(
      'Error: プラグイン設定が見つかりません'
    );
  }

  // ビルド設定のチェック
  if (config.build && !config.build.outDir) {
    throw new Error(
      'Error: ビルド出力ディレクトリが指定されていません'
    );
  }
};

エラーメッセージには具体的な内容を記載することで、問題解決が迅速になります。

typescript// vite.config.ts でバリデーションを実行
import { validateConfig } from './config/validation';

export default defineConfig(({ mode }) => {
  // ... 設定のマージ処理

  const finalConfig = mergeConfig(baseConfig, envConfig);

  // 設定のバリデーション
  validateConfig(finalConfig);

  return finalConfig;
});

設定を返却する前にバリデーションを実行することで、不正な設定での起動を防げます。

まとめ

大規模案件における vite.config.ts の設計では、環境別・役割別に設定を分割することで、保守性と可読性を大幅に向上させることができます。

本記事でご紹介した設計手法のポイントを整理すると、以下のようになります。

設計の要点

#項目内容
1環境別分割開発・ステージング・本番で設定ファイルを分離
2役割別分割プラグイン、ビルド、エイリアスなど機能ごとに分離
3共通設定の抽出全環境で共通する設定は base.ts に集約
4ディレクトリ構成config/ ディレクトリ配下に体系的に配置
5mergeConfig の活用Vite 標準機能で設定を統合
6型定義の共有TypeScript の型を活用して安全性を確保

得られるメリット

設定を分割することで、以下のようなメリットが得られます。

可読性の向上により、新メンバーでも設定の全体像を素早く把握できるようになります。各ファイルの責務が明確なため、必要な設定をすぐに見つけられるでしょう。

保守性の向上も大きなメリットです。特定の機能に関する変更を行う際、影響範囲が限定されるため、安全に修正を加えられます。

チーム開発の効率化も実現できます。複数の開発者が同時に異なる設定ファイルを編集できるため、Git のコンフリクトが発生しにくくなるのです。

環境管理の明確化により、各環境でどの設定が有効になっているか一目瞭然になります。環境固有の問題が発生した際も、該当する設定ファイルだけを確認すれば済みますね。

導入時の注意点

設定分割を導入する際は、以下の点に注意してください。

プロジェクトの規模に応じて、分割の粒度を調整することが重要です。小規模なプロジェクトで過度に分割すると、かえって複雑になってしまいます。

また、チームメンバー全員が設定ファイルの構造を理解できるよう、ドキュメント化やコメントを充実させることも大切でしょう。

設定の変更時には、必ず複数の環境でテストを行い、意図しない影響が出ていないか確認してください。

次のステップ

本記事でご紹介した設計手法を基盤として、さらに以下のような拡張も検討できます。

  • 設定のプリセット化:よく使う設定の組み合わせをプリセットとして定義
  • 動的設定の読み込み:外部ファイルから設定を読み込む仕組みの追加
  • 設定のテスト:設定ファイル自体のユニットテストの導入
  • CI/CD との連携:ビルドパイプラインでの環境別設定の自動適用

大規模なフロントエンド開発では、ビルド設定の管理も重要な設計課題の 1 つです。適切な設計により、開発体験を向上させ、プロジェクトの成功につなげていきましょう。

関連リンク