Viteを極める:高性能なバンドルとカスタムプラグイン開発

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

誰も望んでいなかった「30秒のコーヒー休憩」

数年前、私はファイルを保存するたびに20秒のリビルドが発生するReactプロジェクトに携わっていました。コーヒーを淹れに行くには十分な時間ですが、集中力を完全に切らすのにも十分な時間でした。標準的なWebpack構成を使用していましたが、コンポーネント数が500を超えたあたりで限界を迎え、開発のフィードバックループが完全に崩壊してしまったのです。

この遅延は単なる小さな不便ではなく、生産性を著しく低下させます。フロー状態にあるとき、Hot Module Replacement(HMR)を10秒待つのは永遠のように感じられます。無意識にスマホをチェックしたり新しいタブを開いたりしてしまい、せっかくの勢いが消えてしまいます。多くのチームはこの遅さを「大規模プロジェクトなら避けられない代償」として受け入れていますが、それは私たちが乗り越えるべき誤解です。

レガシーなバンドラーが限界に直面する理由

低速な開発サーバーを改善するには、ボトルネックがどこにあるかを理解する必要があります。WebpackやRollupのようなツールは、ブラウザがネイティブにモジュールをサポートする前に設計されました。それらは、本が1冊返却されるたびに図書館全体のインデックスを再作成する几帳面な司書のような動きをします。サーバーが起動する前に、依存関係グラフ全体をクロールし、すべてのSassファイルを処理し、数千のモジュールを巨大なバンドルに統合します。

プロジェクトの規模が100ファイルから5,000ファイルへと拡大するにつれ、複雑さは指数関数的に増大します。インクリメンタルビルド(増分ビルド)を使用しても、リロードのたびにブラウザが解析しなければならないJavaScriptの量は膨大になり、大きなボトルネックとなります。ファイルを繋ぎ合わせるオーバーヘッドは、プロジェクトの成長とともに勝ち目のない戦いへと変わっていきます。

転換点:WebpackからViteへ

Webpackはその圧倒的な柔軟性によって業界標準の地位を確立しました。しかし、その柔軟性には通常、ローダーの調整に何時間も費やすという「設定のコスト」が伴います.Viteはブラウザのネイティブ機能を活用することで、根本的に異なるアプローチをとっています。

Viteは開発中のバンドル工程を完全にスキップします。ソースコードをネイティブES Modules(ESM)として提供するのです。ブラウザがimport文を検出すると、Viteサーバーからその特定のファイルをオンデマンドで要求します。この手法により、プロジェクトの規模に関係なく、サーバーの起動時間はほぼ瞬時になります。本番環境では、ViteはRollupに切り替わり、軽量で高性能なアセットを生成するために微調整されたビルドを行います。

本番環境に向けた実践的なVite最適化

標準設定だけでは、トラフィックの多いアプリケーションには不十分なことがよくあります。調整せずにnpm run buildを実行するだけでは、肥大化した「ベンダーの塊(vendor blob)」を配信することになり、Core Web Vitalsに悪影響を及ぼすリスクがあります。私の経験上、いくつかの戦略的な調整を行うだけで、初期ロード時間を40%以上短縮できることがよくあります。

1. 戦略的なマニュアルチャンキング

デフォルトでは、Viteはすべての依存関係を1つのvendor.jsファイルにまとめてしまうことがあります。これではキャッシュの効率が損なわれます。単一のユーティリティライブラリを更新しただけで、ユーザーは800KBものベンダーバンドル全体を再ダウンロードせざるを得なくなります。これを解決するために、vite.config.tsで安定したライブラリを独自のチャンクに分離します。

import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            // React関連を個別のチャンクに分離
            if (id.includes('react')) return 'vendor-react';
            // ユーティリティ系を別のチャンクに分離
            if (id.includes('lodash') || id.includes('axios')) return 'vendor-utils';
            // それ以外の依存関係
            return 'vendor'; 
          }
        },
      },
    },
  },
});

2. 高度な圧縮の有効化

最近のブラウザは、標準的なGzipよりもBrotli圧縮をはるかに効率的に処理します。Brotliを使用すれば、150KBのCSSファイルをわずか25KBまで縮小できることもあります。Viteはデフォルトではファイルを圧縮しないため、vite-plugin-compressionをビルドパイプラインに組み込むべきです。

bash
# プラグインのインストール
npm install vite-plugin-compression --save-dev

最良の結果を得るために、Brotliを優先するように設定を更新します:

import viteCompression from 'vite-plugin-compression';

export default defineConfig({
  // Brotli圧縮を有効化し、拡張子を .br に設定
  plugins: [viteCompression({ algorithm: 'brotliCompress', ext: '.br' })],
});

特定のニーズに合わせたカスタムプラグインの作成

ViteはRollupのプラグインインターフェースを使用しているため、拡張が驚くほど簡単です。ニッチな要件があっても、コミュニティのプラグインを待つ必要はありません。例として、ステージング環境でのデプロイ確認に役立つよう、ビルド時のタイムスタンプをHTMLに注入するプラグインを作成してみましょう。

// vite-plugin-timestamp.ts
export default function timestampPlugin() {
  return {
    name: 'timestamp-plugin',
    transformIndexHtml(html) {
      const now = new Date().toISOString();
      // headタグの直後にビルド時刻のメタタグを挿入
      return html.replace(
        '<head>',
        `<head><meta name="build-timestamp" content="${now}">`
      );
    },
  };
}

このフックベースのシステムにより、ビルドのさまざまな段階に介入できます。わずか数行のTypeScriptで、コードの変換、カスタムパスの解決、最終出力の変更などが可能です。

成果を監査する

視覚的なデータがなければ、最適化は単なる推測にすぎません。私は「依存関係の肥大化」を察知するために、常にプロジェクトにrollup-plugin-visualizerを含めています。これは、どのライブラリが最もスペースを消費しているかを正確に示す、インタラクティブなツリーマップを生成します。

import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    visualizer({
      open: true, // ビルド後に自動でブラウザを開く
      filename: 'stats.html',
      gzipSize: true,
    }),
  ],
});

ビルドを実行すると、バンドルの構造を示すブラウザタブが開きます。もし、特定のマイナーなコンポーネントでしか使われていないライブラリが巨大なブロックとして表示されていたら、それは遅延読み込み(lazy loading)を実装するか、より軽量な代替案を探すべき合図です。

Viteへの移行は、重いディーゼルエンジンを電気モーターに載せ替えるようなものです。より静かで、速く、そして効率的です。マニュアルチャンキングとカスタムプラグインをマスターすることで、本番環境のアプリを軽量に保ちつつ、開発環境の高速性を維持できます。ビルドツールとの格闘はやめて、それらをワークフローを加速させるための力に変えましょう。

Share: