WebpackからViteへの脱却:ESMの壁を「最適化ハック」で突破し、ビルドパイプラインを掌握せよ
Webpackの重厚なバンドルプロセスからViteのESMネイティブな高速フィードバックループへ移行する際、多くのエンジニアが「CommonJSの亡霊」に足元を掬われる。これは単なる設定ミスではない。「ブラウザが直接実行可能なESMの厳格さ」と「Webpackが許容していた曖昧なモジュール解決の歴史的遺産」の衝突である。
今回は、レガシーライブラリをViteのアーキテクチャに強制適応させ、CI/CDパイプラインを極限まで最適化するための「現場の知見」を解剖する。
—
1. なぜ「CommonJS」がViteで暴発するのか:内部メカニズムの真実
Viteは開発中、Rollupとesbuildを使い分け、モジュールをブラウザで読み込めるESMへと変換する。ここで重要なのは、Viteは依存関係を「事前バンドル(Pre-bundling)」しているという点だ。
Webpackはビルド時に `node_modules` を再帰的に探索し、CommonJSを強引にラップして一つの塊にする。一方、Viteは `node_modules/.vite` 内にエントリポイントを作り、依存関係をESMとしてキャッシュする。この「事前バンドル」のプロセスで、非標準的なCJSライブラリ(`exports` の動的書き換えや、`this` のグローバル参照を行うもの)が読み込めない。
これを解決する定石は、`optimizeDeps` による強制変換だ。
// vite.config.ts
export default defineConfig({
optimizeDeps: {
// 依存関係が複雑なライブラリを明示的に指定して事前バンドル対象にする
include: [‘legacy-library-a’, ‘another-broken-module’],
esbuildOptions: {
// 内部の esbuild が CommonJS を ESM に変換する際の挙動を強制する
plugins: [
{
name: ‘load-cjs-as-esm’,
setup(build) {
build.onLoad({ filter: /legacy-library-a.\.js$/ }, (args) => ({
contents: `import { createRequire } from ‘module’; const require = createRequire(import.meta.url); ${require(‘fs’).readFileSync(args.path, ‘utf8’)}`,
loader: ‘js’,
}));
},
},
],
},
},
});
このアプローチの真髄は、Viteのビルドキャッシュを「汚す」のではなく「注入する」点にある。これにより、CI/CD環境での `npm install` 後に発生する「初回ビルドの遅延」を最小化できる。
—
2. DockerコンテナとCI/CDにおける「キャッシュ汚染」の排除
大規模プロジェクトでViteへの移行を行う際、最も手痛いのが「Dockerビルド時のキャッシュ破綻」だ。`node_modules/.vite` はローカル環境のOSやNode.jsバージョンに依存する。
CI/CDパイプライン(GitHub Actions等)では、以下の構成で環境を固定し、ビルド時間を短縮する。
.github/workflows/build.yml
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ’20’
cache: ‘npm’
# Viteの依存関係キャッシュを明示的に永続化する
- name: Cache Vite Pre-bundling
uses: actions/cache@v3
with:
path: node_modules/.vite
key: ${{ runner.os }}-vite-${{ hashFiles(‘package-lock.json’) }}
- name: Build
run: npm run build
アーキテクトの助言: `node_modules/.vite` をキャッシュする際は、`package-lock.json` のハッシュ値だけでなく、`vite.config.ts` のハッシュ値もキーに含めるべきだ。設定変更時にキャッシュが古いままだと、謎のランタイムエラーがCI上で発生し、デバッグの地獄を味わうことになる。
—
3. レガシーライブラリの断捨離:プラグインを超えた戦略的アプローチ
`vite-plugin-commonjs` は強力だが、あくまで対症療法だ。真にパフォーマンスを追求するなら、「ビルド時に変換する」のではなく「ランタイムで別出しにする」手法をとる。
特定モジュールの「外部化(Externalize)」
どうしてもESMに変換できないモジュールは、`rollupOptions` を使い、CDNから読み込むか、静的アセットとして `public` ディレクトリに退避させる。
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
// バンドル対象から除外することで、ビルド時間を劇的に短縮する
external: [‘legacy-module-name’],
output: {
paths: {
‘legacy-module-name’: ‘/scripts/legacy-module.min.js’
}
}
}
}
});
この手法を採用すると、メインのJSバンドルサイズが劇的に縮小し、Lighthouseスコアが向上する。さらに、そのレガシーJSはブラウザキャッシュが効きやすいため、ユーザー体験も向上する。「すべてをViteでバンドルすべき」という思考を捨てることこそ、真のアーキテクトの選択だ。
—
結び:ツールに支配されるな、パイプラインを支配せよ
Viteへの移行は、単なるビルドツールの乗り換えではない。それは、過去10年間のWeb開発の負債を清算し、ESMという現代の標準にコードベースを適合させる「外科手術」である。
1. `optimizeDeps` でライブラリを掌握する
2. Dockerキャッシュ戦略でパイプラインの再現性を担保する
3. 解決不能なレガシーは外部化(Externalize)で切り離す
これらを徹底すれば、あなたのビルドパイプラインはWebpack時代には到底到達できなかった、秒速単位のフィードバックループを実現するだろう。ツールを使いこなすのではない。ツールが勝手に最適化されるような「環境の設計図」を描く。それが、現場で戦う我々の責務である。