T-CREATOR

Vite の build コマンド徹底攻略:最適な本番ビルドを目指す

Vite の build コマンド徹底攻略:最適な本番ビルドを目指す

現代の Web 開発において、Vite は開発体験と本番パフォーマンスの両立を実現する革新的なビルドツールとして注目を集めています。特に本番ビルドでは、アプリケーションの最適化が開発者の手腕によって大きく左右されるため、適切な設定と戦略的なアプローチが不可欠です。

本記事では、Vite の build コマンドを徹底的に活用し、最適な本番ビルドを実現するための実践的な手法をご紹介いたします。基本的な設定から高度な最適化テクニックまで、実際のプロジェクトで即座に活用できる具体的な実装例とともに解説していきます。

Vite のビルドシステムの基本概念

ビルドプロセスの全体像

Vite のビルドシステムは、開発モードと本番モードで異なるアプローチを採用しています。開発時には ESM(ES Modules)を活用した高速な開発サーバーを提供し、本番ビルド時には Rollup をベースとした高度な最適化を実行します。

この二重構造により、開発効率と本番パフォーマンスの両方を最大化できますが、その恩恵を最大限に受けるには、ビルド設定の理解と適切な調整が重要となります。

本番ビルドの重要性

本番ビルドの品質は、以下の要素に直接的な影響を与えます:

パフォーマンス指標への影響: First Contentful Paint(FCP)、Largest Contentful Paint(LCP)などの Core Web Vitals に大きく影響し、SEO やユーザー体験の向上に直結します。

リソース効率性: 適切なコード分割とアセット最適化により、初期ロード時間を短縮し、ネットワーク負荷を軽減できます。

スケーラビリティ: 効率的なキャッシュ戦略により、アプリケーションの成長に対応した持続可能なパフォーマンスを実現できます。

セキュリティ強化: 本番環境に不要な開発用コードの除去や、機密情報の適切な処理により、セキュリティリスクを最小化できます。

ビルド設定の基本

vite.config.ts の基本構造

効果的なビルド設定の出発点として、以下の構成を推奨いたします:

typescriptimport { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';

export default defineConfig(({ command, mode }) => {
  const env = loadEnv(mode, process.cwd(), '');

  return {
    plugins: [react()],
    resolve: {
      alias: {
        '@': resolve(__dirname, 'src'),
      },
    },
    build: {
      outDir: 'dist',
      assetsDir: 'assets',
      minify: 'terser',
      sourcemap: false,
      target: 'es2015',
      cssTarget: 'chrome80',
      rollupOptions: {
        output: {
          manualChunks: {
            vendor: ['react', 'react-dom'],
          },
        },
      },
    },
    server: {
      port: 3000,
      open: true,
    },
  };
});

この設定では、基本的な最適化を実現しつつ、拡張性を考慮した構造となっています。

主要なビルドオプションの解説

各オプションの詳細な説明と推奨設定値をご紹介します:

項目オプション名説明推奨設定考慮点
# 1outDirビルド出力先ディレクトリdistCI/CD との連携を考慮
# 2assetsDir静的アセットのサブディレクトリassetsCDN 配信を想定した構造
# 3minifyコード圧縮の方式terser高い圧縮率と互換性
# 4sourcemapソースマップの生成本番では falseセキュリティとサイズのバランス
# 5targetターゲット環境es2015幅広いブラウザサポート

詳細設定のベストプラクティス

typescriptexport default defineConfig({
  build: {
    // パフォーマンス重視の設定
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true, // console.log を除去
        drop_debugger: true, // debugger を除去
      },
    },

    // チャンクサイズの制御
    chunkSizeWarningLimit: 1000,

    // アセットファイル名の制御
    rollupOptions: {
      output: {
        chunkFileNames: 'js/[name]-[hash].js',
        entryFileNames: 'js/[name]-[hash].js',
        assetFileNames: ({ name }) => {
          if (/\.(gif|jpe?g|png|svg)$/.test(name ?? '')) {
            return 'images/[name]-[hash][extname]';
          }
          if (/\.css$/.test(name ?? '')) {
            return 'css/[name]-[hash][extname]';
          }
          return 'assets/[name]-[hash][extname]';
        },
      },
    },
  },
});

パフォーマンス最適化

コード分割の設定

効果的なコード分割は、初期ロード時間の短縮とキャッシュ効率の向上に不可欠です。

戦略的なチャンク分割

typescriptexport default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // React エコシステム
          'react-vendor': ['react', 'react-dom'],

          // ルーティング関連
          router: ['react-router-dom'],

          // UI コンポーネントライブラリ
          'ui-lib': ['@mui/material', '@emotion/react'],

          // ユーティリティライブラリ
          utils: ['lodash', 'date-fns'],

          // 状態管理
          state: ['zustand', 'jotai'],
        },
      },
    },
  },
});

動的チャンク分割

typescriptexport default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          // node_modules を vendor チャンクに
          if (id.includes('node_modules')) {
            // 大きなライブラリを個別分割
            if (
              id.includes('react') ||
              id.includes('react-dom')
            ) {
              return 'react-vendor';
            }
            if (id.includes('@mui')) {
              return 'mui-vendor';
            }
            if (id.includes('lodash')) {
              return 'utils-vendor';
            }
            return 'vendor';
          }

          // 機能別チャンク分割
          if (id.includes('src/pages/')) {
            const page = id
              .split('/pages/')[1]
              .split('/')[0];
            return `page-${page}`;
          }

          if (id.includes('src/features/')) {
            const feature = id
              .split('/features/')[1]
              .split('/')[0];
            return `feature-${feature}`;
          }
        },
      },
    },
  },
});

チャンクサイズの最適化

理想的なチャンクサイズの実現

適切なチャンクサイズは、ネットワーク効率とキャッシュ効果のバランスを取ることが重要です:

typescriptexport default defineConfig({
  build: {
    // 警告閾値を調整
    chunkSizeWarningLimit: 500,

    rollupOptions: {
      output: {
        manualChunks(id) {
          const chunkSize = {
            'react-vendor': ['react', 'react-dom'],
            'large-lib': [], // 大きなライブラリ用
            common: [], // 共通モジュール用
          };

          // サイズベースの分割ロジック
          if (id.includes('node_modules')) {
            // 大きなライブラリの識別と分割
            const largeLibs = ['@mui', 'antd', 'recharts'];
            const matchedLib = largeLibs.find((lib) =>
              id.includes(lib)
            );

            if (matchedLib) {
              return `vendor-${matchedLib
                .replace('@', '')
                .replace('/', '-')}`;
            }

            return 'vendor';
          }
        },
      },
    },
  },
});

ツリーシェイキングの活用

高度なツリーシェイキング設定

不要なコードの除去を最大化するための設定をご紹介します:

typescriptexport default defineConfig({
  build: {
    rollupOptions: {
      treeshake: {
        moduleSideEffects: false,
        propertyReadSideEffects: false,
        unknownGlobalSideEffects: false,
      },
      external: (id) => {
        // 外部依存として扱うモジュールの指定
        return id.startsWith('virtual:');
      },
    },
  },

  // プラグインレベルでの最適化
  plugins: [
    react({
      // React の最適化オプション
      babel: {
        plugins: [
          [
            'babel-plugin-transform-remove-console',
            { exclude: ['error', 'warn'] },
          ],
        ],
      },
    }),
  ],
});

ライブラリ別の最適化設定

typescript// 効率的な import 文の活用例
// ❌ 非効率な import
import _ from 'lodash';

// ✅ 効率的な import
import { debounce, throttle } from 'lodash';

// vite.config.ts での対応
export default defineConfig({
  optimizeDeps: {
    include: [
      'lodash/debounce',
      'lodash/throttle',
      // 必要な関数のみを明示的に指定
    ],
  },
});

アセット最適化

画像の最適化設定

次世代画像形式の活用

typescriptimport { defineConfig } from 'vite';
import { imagetools } from 'vite-imagetools';

export default defineConfig({
  plugins: [
    imagetools({
      defaultDirectives: new URLSearchParams([
        ['format', 'avif;webp;jpg'],
        ['quality', '80'],
        ['as', 'picture'],
      ]),
    }),
  ],

  build: {
    rollupOptions: {
      output: {
        assetFileNames: (assetInfo) => {
          const info = assetInfo.name.split('.');
          const ext = info[info.length - 1];

          if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(ext)) {
            return `images/[name]-[hash][extname]`;
          }
          return `assets/[name]-[hash][extname]`;
        },
      },
    },
  },
});

レスポンシブ画像の自動生成

typescript// 使用例:画像の自動最適化
// src/components/OptimizedImage.tsx
import imageUrl from '../assets/hero.jpg?w=400;800;1200&format=webp&as=srcset';

function OptimizedImage() {
  return (
    <picture>
      <source srcSet={imageUrl} type='image/webp' />
      <img
        src='../assets/hero.jpg'
        alt='Hero image'
        loading='lazy'
      />
    </picture>
  );
}

CSS の最適化

PostCSS による高度な最適化

typescriptexport default defineConfig({
  css: {
    postcss: {
      plugins: [
        // ベンダープレフィックスの自動付与
        require('autoprefixer')({
          overrideBrowserslist: ['> 1%', 'last 2 versions'],
        }),

        // CSS の圧縮と最適化
        require('cssnano')({
          preset: [
            'advanced',
            {
              discardComments: { removeAll: true },
              reduceIdents: false,
              zindex: false,
            },
          ],
        }),

        // 未使用 CSS の除去
        require('@fullhuman/postcss-purgecss')({
          content: ['./src/**/*.{js,jsx,ts,tsx}'],
          defaultExtractor: (content) =>
            content.match(/[\w-/:]+(?<!:)/g) || [],
        }),
      ],
    },

    // CSS Modules の最適化
    modules: {
      localsConvention: 'camelCase',
      generateScopedName: '[local]_[hash:base64:5]',
    },
  },
});

フォントの最適化

Web フォントの効率的な配信

typescriptexport default defineConfig({
  build: {
    rollupOptions: {
      output: {
        assetFileNames: (assetInfo) => {
          // フォントファイルの最適化
          if (
            /\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(
              assetInfo.name
            )
          ) {
            return 'fonts/[name][extname]';
          }
          return 'assets/[name]-[hash][extname]';
        },
      },
    },
  },

  // フォント preload の設定
  plugins: [
    {
      name: 'font-preload',
      generateBundle(options, bundle) {
        // 重要なフォントファイルのpreload設定
        const fontFiles = Object.keys(bundle).filter(
          (fileName) => /\.(woff2?)$/.test(fileName)
        );

        // HTML に preload リンクを挿入
        fontFiles.forEach((fontFile) => {
          // preload 設定のロジック
        });
      },
    },
  ],
});

環境変数とビルド

環境別の設定

包括的な環境管理

typescript// vite.config.ts
import { defineConfig, loadEnv } from 'vite';

export default defineConfig(({ command, mode }) => {
  const env = loadEnv(mode, process.cwd(), '');

  // 環境別の設定オブジェクト
  const envConfig = {
    development: {
      sourcemap: true,
      minify: false,
      cssCodeSplit: false,
    },
    staging: {
      sourcemap: true,
      minify: 'terser',
      cssCodeSplit: true,
    },
    production: {
      sourcemap: false,
      minify: 'terser',
      cssCodeSplit: true,
      reportCompressedSize: false,
    },
  };

  const currentConfig =
    envConfig[mode] || envConfig.production;

  return {
    define: {
      __APP_VERSION__: JSON.stringify(
        process.env.npm_package_version
      ),
      __BUILD_TIME__: JSON.stringify(
        new Date().toISOString()
      ),
      __API_BASE_URL__: JSON.stringify(
        env.VITE_API_BASE_URL
      ),
    },

    build: {
      ...currentConfig,
      rollupOptions: {
        output: {
          // 環境に応じたファイル名戦略
          entryFileNames:
            mode === 'production'
              ? 'js/[name]-[hash].js'
              : 'js/[name].js',
        },
      },
    },
  };
});

機密情報の取り扱い

セキュアな環境変数管理

typescript// セキュリティを重視した環境変数の処理
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '');

  // 安全な環境変数のみを抽出
  const safeEnvVars = Object.keys(env)
    .filter((key) => key.startsWith('VITE_'))
    .reduce((acc, key) => {
      acc[key] = env[key];
      return acc;
    }, {});

  return {
    define: {
      // 安全な変数のみを define に含める
      ...Object.keys(safeEnvVars).reduce((acc, key) => {
        acc[`process.env.${key}`] = JSON.stringify(
          safeEnvVars[key]
        );
        return acc;
      }, {}),
    },

    build: {
      rollupOptions: {
        // 機密情報を含む可能性のあるコメントを除去
        output: {
          banner:
            '/* This build contains only public information */',
        },
        plugins: [
          {
            name: 'remove-sensitive-comments',
            renderChunk(code) {
              // 機密情報を含む可能性のあるコメントのパターン
              return code.replace(
                /\/\*[\s\S]*?(password|secret|key|token)[\s\S]*?\*\//gi,
                ''
              );
            },
          },
        ],
      },
    },
  };
});

ビルドプロセスのカスタマイズ

プラグインの活用

高度な最適化プラグインの組み合わせ

typescriptimport { defineConfig } from 'vite';
import { compression } from 'vite-plugin-compression';
import { visualizer } from 'rollup-plugin-visualizer';
import { createHtmlPlugin } from 'vite-plugin-html';

export default defineConfig({
  plugins: [
    // HTML の最適化
    createHtmlPlugin({
      minify: {
        collapseWhitespace: true,
        keepClosingSlash: true,
        removeComments: true,
        removeRedundantAttributes: true,
        removeScriptTypeAttributes: true,
        removeStyleLinkTypeAttributes: true,
        useShortDoctype: true,
        minifyCSS: true,
        minifyJS: true,
      },
    }),

    // 複数形式での圧縮
    compression({
      algorithm: 'gzip',
      ext: '.gz',
      threshold: 1024,
    }),
    compression({
      algorithm: 'brotliCompress',
      ext: '.br',
      threshold: 1024,
    }),

    // バンドル分析
    visualizer({
      filename: 'dist/stats.html',
      open: false,
      gzipSize: true,
      brotliSize: true,
      template: 'treemap',
    }),
  ],
});

カスタムビルドスクリプト

高度なビルドパイプライン

typescript// scripts/build.ts
import { build } from 'vite';
import { resolve } from 'path';
import { promises as fs } from 'fs';
import { gzipSync } from 'zlib';

interface BuildMetrics {
  totalSize: number;
  gzipSize: number;
  chunkCount: number;
  buildTime: number;
}

async function runOptimizedBuild(): Promise<BuildMetrics> {
  const startTime = Date.now();

  try {
    console.log('🚀 Starting optimized build process...');

    // 1. ビルド前の準備
    await fs.rm(resolve(__dirname, '../dist'), {
      recursive: true,
      force: true,
    });

    // 2. メインビルドの実行
    const result = await build({
      configFile: resolve(__dirname, '../vite.config.ts'),
      mode: 'production',
      logLevel: 'info',
    });

    // 3. ビルド結果の分析
    const metrics = await analyzeBundle();

    // 4. 最適化後処理
    await postBuildOptimization();

    console.log('✅ Build completed successfully!');
    console.log(`📊 Build metrics:`, metrics);

    return metrics;
  } catch (error) {
    console.error('❌ Build failed:', error);
    process.exit(1);
  }
}

async function analyzeBundle(): Promise<BuildMetrics> {
  const distPath = resolve(__dirname, '../dist');
  const files = await fs.readdir(distPath, {
    recursive: true,
  });

  let totalSize = 0;
  let gzipSize = 0;
  let chunkCount = 0;

  for (const file of files) {
    if (typeof file === 'string' && file.endsWith('.js')) {
      const filePath = resolve(distPath, file);
      const content = await fs.readFile(filePath);

      totalSize += content.length;
      gzipSize += gzipSync(content).length;
      chunkCount++;
    }
  }

  return {
    totalSize,
    gzipSize,
    chunkCount,
    buildTime: Date.now() - startTime,
  };
}

async function postBuildOptimization(): Promise<void> {
  // カスタム最適化処理
  console.log('🔧 Running post-build optimizations...');

  // Service Worker の生成
  await generateServiceWorker();

  // 静的アセットの最適化
  await optimizeStaticAssets();

  // セキュリティヘッダーの設定ファイル生成
  await generateSecurityHeaders();
}

runOptimizedBuild();

デプロイメント最適化

各種ホスティングサービス向けの設定

Vercel 向けの最適化設定

json{
  "builds": [
    {
      "src": "package.json",
      "use": "@vercel/static-build",
      "config": {
        "distDir": "dist",
        "buildCommand": "yarn build"
      }
    }
  ],
  "routes": [
    {
      "src": "/assets/(.*)",
      "headers": {
        "cache-control": "public, max-age=31536000, immutable"
      }
    },
    {
      "src": "/(.*)",
      "dest": "/index.html"
    }
  ],
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        },
        {
          "key": "X-Frame-Options",
          "value": "DENY"
        }
      ]
    }
  ]
}

Netlify 向けの最適化設定

toml[build]
  command = "yarn build"
  publish = "dist"

# アセットの長期キャッシュ設定
[[headers]]
  for = "/assets/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

# セキュリティヘッダー
[[headers]]
  for = "/*"
  [headers.values]
    X-Frame-Options = "DENY"
    X-Content-Type-Options = "nosniff"
    Referrer-Policy = "strict-origin-when-cross-origin"

# SPA routing
[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200
  conditions = {Role = ["admin","editor"], Country = ["US"]}

CDN の活用

効率的な CDN 配信戦略

typescript// 環境に応じた CDN 設定
export default defineConfig(({ mode }) => {
  const cdnConfig = {
    production: 'https://cdn.example.com',
    staging: 'https://staging-cdn.example.com',
    development: '',
  };

  const cdnBase = cdnConfig[mode] || '';

  return {
    base: cdnBase,

    build: {
      rollupOptions: {
        output: {
          assetFileNames: (assetInfo) => {
            const info = assetInfo.name.split('.');
            const ext = info[info.length - 1];

            // CDN に適したファイル構造
            if (/png|jpe?g|svg|gif/i.test(ext)) {
              return `images/[name]-[hash][extname]`;
            }
            if (/css/.test(ext)) {
              return `styles/[name]-[hash][extname]`;
            }
            return `assets/[name]-[hash][extname]`;
          },
        },
      },
    },

    // CDN プリロード設定
    plugins: [
      {
        name: 'cdn-preload',
        generateBundle() {
          // 重要なアセットの CDN プリロード設定
        },
      },
    ],
  };
});

トラブルシューティング

一般的なビルドエラーと解決策

実際のプロジェクトでよく遭遇するエラーとその対処法をまとめました:

項目エラーの種類主な原因解決方法予防策
# 1チャンクサイズ警告大きすぎるバンドルmanualChunks による分割定期的なバンドル分析
# 2メモリ不足エラー大量のアセット処理Node.js メモリ制限の調整段階的ビルドの実装
# 3循環依存エラーモジュール間の循環参照依存関係の再設計ESLint ルールの追加
# 4型エラーTypeScript の型チェックskipLibCheck の適切な使用型定義の整備

具体的な解決例

typescript// メモリ不足の解決
export default defineConfig({
  build: {
    // 大きなプロジェクト向けの設定
    chunkSizeWarningLimit: 1500,
    rollupOptions: {
      // メモリ効率的な処理
      maxParallelFileOps: 2,
      output: {
        manualChunks: (id) => {
          // 効率的なチャンク分割
          if (id.includes('node_modules')) {
            return 'vendor'
          }
        },
      },
    },
  },
})

// package.json でのメモリ制限調整
{
  "scripts": {
    "build": "node --max-old-space-size=4096 ./node_modules/vite/bin/vite.js build"
  }
}

パフォーマンス問題の診断

包括的なパフォーマンス分析

typescriptimport { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    // 詳細なバンドル分析
    visualizer({
      filename: 'dist/analysis/bundle-analysis.html',
      open: true,
      gzipSize: true,
      brotliSize: true,
      template: 'treemap', // sunburst, network, raw-data
    }),

    // ビルド時間の分析
    {
      name: 'build-timer',
      buildStart() {
        this.buildStart = Date.now();
      },
      generateBundle() {
        const buildTime = Date.now() - this.buildStart;
        console.log(`⏱️ Build time: ${buildTime}ms`);

        // ビルド時間をファイルに記録
        this.emitFile({
          type: 'asset',
          fileName: 'build-metrics.json',
          source: JSON.stringify({
            buildTime,
            timestamp: new Date().toISOString(),
          }),
        });
      },
    },
  ],
});

まとめ

Vite の build コマンドを最適化することで、以下の具体的な成果を実現できます:

パフォーマンスの大幅な改善: 適切なコード分割とアセット最適化により、初期ロード時間を 30-50% 短縮し、Core Web Vitals の改善につながります。

開発効率の向上: 環境別の設定管理とカスタムビルドスクリプトにより、デプロイメントプロセスが自動化され、開発チームの生産性が向上します。

保守性の強化: 体系的なビルド設定により、プロジェクトの成長に対応した持続可能な開発体制を構築できます。

セキュリティの確保: 適切な環境変数管理と本番向けの最適化により、セキュリティリスクを最小限に抑えることができます。

これらの最適化手法を段階的に導入することで、Vite の真の力を引き出し、ユーザーにとって最高の体験を提供する Web アプリケーションを構築していきましょう。適切なビルド設定は、技術的な投資として長期的な価値をもたらし、プロジェクトの成功に大きく貢献いたします。

関連リンク