ViteビルドをCIのボトルネックにしない:GitHub Actionsにおける「極限の最適化」解剖学
フロントエンドのビルドツールがWebpackからViteへ移行したことで、ローカル開発体験は劇的に向上した。しかし、CI/CDパイプラインにその恩恵を持ち込めているかと言えば、話は別だ。多くのエンジニアが「ビルドがたまに死ぬ」「キャッシュが効かない」「メモリ不足でプロセスが強制終了する」といった泥沼に陥っている。
本稿では、Viteの内部アーキテクチャを理解した上で、GitHub Actionsを単なるCIツールから「高速なビルドマシン」へと昇華させるためのアーキテクチャ設計を解説する。
—
1. Node.jsのバージョン固定と「アーティファクトの不変性」
CI環境で最も初歩的かつ致命的なミスは、`actions/setup-node`でバージョンを曖昧に指定することだ。
- uses: actions/setup-node@v4
with:
node-version-file: ‘.node-version’ # 常にプロジェクトの規定に縛る
cache: ‘npm’ # パッケージマネージャを明示
なぜこれが重要か。Viteは内部でRollupを使用しており、Node.jsのバージョンによってネイティブモジュールのコンパイル結果やメモリ管理の挙動が微妙に異なる。特に`esbuild`のバイナリはプラットフォーム依存が激しい。`.node-version`等のファイルをプロジェクトルートに配置し、CIとローカルでバイナリ互換性を100%一致させることは、「ビルドの再現性」というDevOpsの聖域を守るための最低条件だ。
—
2. キャッシュの「再定義」:`node_modules`だけでは不十分だ
GitHub Actionsの標準的な`cache`アクションは、`node_modules`のハッシュ値を見るだけだ。しかし、Viteのビルド速度を支配しているのは実は「キャッシュされた依存関係」ではなく、`node_modules/.vite`ディレクトリである。
Viteは依存関係を事前にバンドル(Pre-bundling)し、`.vite`フォルダに格納する。CI上で毎回これを再生成させるのは、ビルド時間を30秒〜数分ロスする要因だ。
戦略的キャッシュの構成
- name: Cache Vite dependencies
uses: actions/cache@v4
with:
path: |
node_modules/.vite
dist/
# package-lock.jsonとViteの設定変更をトリガーにキャッシュを更新
key: ${{ runner.os }}-vite-${{ hashFiles(‘package-lock.json’, ‘vite.config.ts’) }}
restore-keys: |
${{ runner.os }}-vite-
この設定により、依存関係に変更がない限り、Viteの事前バンドル処理を完全にスキップできる。大規模なモノレポでは、これだけでビルド時間が数分単位で短縮される。
—
3. メモリ消費の最適化:CI環境という「狭い箱」での戦い
GitHub Actionsの標準ランナー(2vCPU / 7GB RAM)は、大規模なフロントエンドプロジェクトにとって「狭い」ことが往々にしてある。Vite(Rollup)が多数のチャンクを生成する際、Node.jsのデフォルトのヒープ制限(通常は4GB程度)に抵触し、`FATAL ERROR: Ineffective mark-compacts near heap limit`で落ちるケースが後を絶たない。
これを防ぐには、CI環境でのみメモリ制限を明示的に拡張する。
jobs:
build:
env:
# Node.jsのヒープメモリ上限を6GBに拡張
NODE_OPTIONS: “–max-old-space-size=6144”
steps:
- run: npm run build
ここで重要なのは、この設定をプロジェクト全体の`.env`に入れるのではなく、CIの環境変数として注入することだ。ローカル開発環境でこの設定を強制すると、開発者のPCメモリを食いつぶし、開発効率を低下させるためである。
—
4. 環境変数の注入:ビルド時と実行時の「境界」を理解する
Viteにおける最大の落とし穴は、`import.meta.env`の注入タイミングだ。Viteはビルド時に環境変数を静的に置換する。CIパイプラインで`process.env`経由で変数を渡す際、GitHub Actionsの`env`コンテキストをそのまま使おうとして失敗する例が多い。
確実な注入スクリプト
環境変数が多岐にわたる場合、`.env`ファイルを動的に生成するスクリプトをパイプラインに組み込むのが最も堅牢だ。
ビルド前にセキュアに環境変数を生成するスクリプト例
cat <
VITE_API_URL=${{ secrets.API_URL }}
VITE_BUILD_TIME=$(date -u +”%Y-%m-%dT%H:%M:%SZ”)
EOF
このアプローチを取ることで、パイプラインの途中で環境変数が変わるリスク(Race condition)を排除し、ビルドの不変性を担保できる。
—
5. 究極のハック:Dockerコンテナでの完全隔離ビルド
もし、ビルド環境の複雑性が増し、GitHubホステッドランナーでは限界を感じたなら、Dockerによるビルドコンテナ化を検討すべきだ。
Dockerfileの最適化
FROM node:20-slim AS builder
WORKDIR /app
COPY package.json ./
RUN npm ci –prefer-offline –no-audit
COPY . .
必要なバイナリのみをビルド環境にコピー
RUN npm run build
GitHub Actions上でこのDockerfileをビルドする際、`–cache-from`オプションを使ってDockerレイヤーキャッシュをGitHub Actionsのキャッシュに紐付けることで、「Dockerレイヤー」と「npmパッケージ」の両面でキャッシュを極限まで効かせることが可能になる。
—
結びに代えて:アーキテクトとしての提言
CIパイプラインは単なる「自動化ツール」ではない。それは「開発者の心理的安全性を担保する防波堤」である。
Viteの爆速ビルドをCIで殺さないためには、以下の3点を徹底してほしい。
1. キャッシュの解像度を上げる(`node_modules`だけでなく`node_modules/.vite`も)。
2. リソース限界を予測する(Node.jsのヒープ上限をCI環境でのみチューニングする)。
3. ビルドの不変性を守る(環境変数の注入を静的かつ再現可能な形で行う)。
これらを実装した瞬間から、あなたのチームのCIは「重くて落ちるもの」から「信頼できる高速なデリバリーエンジン」へと変貌を遂げるはずだ。現場で震えるようなパフォーマンスを、ぜひ体感してほしい。