Vitest 大規模 CI の最適化技術:テストシャーディング × キャッシュヒット最大化
大規模なプロジェクトでは、テストの実行時間が開発サイクルのボトルネックになることがよくあります。数千のテストケースが CI で実行されると、完了まで 10 分、20 分、場合によっては 1 時間以上かかることも珍しくありません。
この記事では、Vitest を使った CI 環境での実行時間を劇的に短縮する 2 つの強力な手法「テストシャーディング」と「キャッシュヒット最大化」を詳しく解説します。これらを組み合わせることで、大規模プロジェクトでも数分でテストを完了させられるようになるでしょう。
背景
大規模プロジェクトにおけるテスト実行の現状
近年のフロントエンド開発では、TypeScript や React などのフレームワークを使った複雑な UI コンポーネントが増え、それに伴ってテストケースの数も急増しています。
数百人規模の開発チームでは、1 日に数十回から数百回の PR がマージされ、その都度すべてのテストを実行する必要があります。テスト実行時間が長いと、開発者はフィードバックを待つ時間が増え、生産性が大きく低下してしまうのです。
CI パイプラインにおけるテストの重要性
CI(Continuous Integration)では、コードの品質を保つためにテストが欠かせません。しかし、テスト実行が遅いと以下のような問題が発生します。
- デプロイまでの時間が長くなり、ビジネス機会を逃す
- 開発者がテスト完了を待つ間、別の作業に切り替えてコンテキストスイッチが発生する
- CI キューが詰まり、他の開発者の作業がブロックされる
以下の図は、従来の単一実行環境でのテストフローを示しています。
mermaidflowchart TD
pr["PR作成"] --> ci_start["CI開始"]
ci_start --> install["依存関係インストール"]
install --> test_all["全テスト実行<br/>(単一プロセス)"]
test_all --> result["結果レポート"]
result --> merge{テスト成功?}
merge -->|成功| done["マージ完了"]
merge -->|失敗| fix["修正"]
fix --> pr
style test_all fill:#ffcccc
style done fill:#ccffcc
このフローでは、すべてのテストが順番に実行されるため、テスト数が増えるほど時間がかかってしまいます。
課題
テスト実行時間の増大がもたらす問題
大規模プロジェクトでのテスト実行には、以下のような具体的な課題があります。
| # | 課題 | 影響 | 発生頻度 |
|---|---|---|---|
| 1 | テスト実行に 20 分以上かかる | 開発者の待ち時間増加 | ★★★ |
| 2 | CI ランナーのリソース不足 | キューの詰まり | ★★★ |
| 3 | 同じ依存関係を毎回インストール | 無駄な時間消費 | ★★★ |
| 4 | 変更していないコードも再テスト | 重複実行による時間浪費 | ★★☆ |
| 5 | テスト失敗時の原因特定に時間がかかる | デバッグ効率の低下 | ★★☆ |
従来の最適化手法の限界
単純な並列実行だけでは、以下の理由で十分な効果が得られません。
一つ目は、単一マシンでの並列実行には CPU コア数の制限があることです。たとえば 8 コアのマシンでは、8 プロセス以上の並列実行は効果が薄くなります。
二つ目は、メモリ使用量の増加により、並列数を増やすとシステムが不安定になる可能性があることです。特に Node.js プロセスは各自がメモリを消費するため、同時実行数には限界があるでしょう。
三つ目は、依存関係のインストールやビルド処理が毎回実行され、その時間が無視できないことです。
以下の図は、従来の最適化手法とその限界を示しています。
mermaidflowchart LR
subgraph traditional["従来の並列実行"]
direction TB
single["単一マシン"] --> cores["8コア制限"]
cores --> mem["メモリ不足"]
mem --> slow["実行時間: 15分"]
end
subgraph issues["主な問題"]
direction TB
i1["スケール限界"]
i2["リソース競合"]
i3["キャッシュ未活用"]
end
traditional --> issues
style slow fill:#ffcccc
style issues fill:#ffffcc
これらの課題を解決するには、複数マシンでの並列実行(シャーディング)と、賢いキャッシュ戦略が必要です。
解決策
テストシャーディングによる分散実行
テストシャーディングとは、テストスイート全体を複数のグループ(シャード)に分割し、それぞれを別々の CI ランナーで並列実行する手法です。
Vitest は --shard オプションを使って、簡単にシャーディングを実現できます。以下のコマンドは、テストを 4 つのシャードに分割し、その 1 番目を実行する例です。
bash# 4つのシャードに分割し、1番目を実行
vitest --shard=1/4
このオプションを使うことで、テストファイルがハッシュ値に基づいて均等に分配されます。各シャードは独立して実行されるため、シャード数を増やすほど全体の実行時間が短縮されるのです。
シャーディングの仕組み
Vitest は内部で以下のようなアルゴリズムでテストを分配します。
typescript// シャーディングアルゴリズムの概念コード
function getShardForFile(
filePath: string,
shardIndex: number,
totalShards: number
): boolean {
// ファイルパスからハッシュ値を生成
const hash = hashCode(filePath);
// ハッシュ値を総シャード数で割った余りでシャードを決定
const assignedShard = (hash % totalShards) + 1;
// 現在のシャードインデックスと一致するか判定
return assignedShard === shardIndex;
}
この方法により、テストファイルが各シャードにほぼ均等に分配され、効率的な並列実行が可能になります。
以下の図は、テストシャーディングの仕組みを示しています。
mermaidflowchart TB
tests["テストスイート<br/>(1000ファイル)"] --> shard1["シャード 1/4<br/>(250ファイル)"]
tests --> shard2["シャード 2/4<br/>(250ファイル)"]
tests --> shard3["シャード 3/4<br/>(250ファイル)"]
tests --> shard4["シャード 4/4<br/>(250ファイル)"]
shard1 --> runner1["CI Runner 1"]
shard2 --> runner2["CI Runner 2"]
shard3 --> runner3["CI Runner 3"]
shard4 --> runner4["CI Runner 4"]
runner1 --> result1["結果: 3分"]
runner2 --> result2["結果: 3分"]
runner3 --> result3["結果: 3分"]
runner4 --> result4["結果: 3分"]
result1 --> aggregate["結果集約"]
result2 --> aggregate
result3 --> aggregate
result4 --> aggregate
style aggregate fill:#ccffcc
キャッシュヒット最大化戦略
CI でのテスト実行を高速化するもう一つの鍵は、キャッシュの効果的な活用です。キャッシュヒット率を最大化することで、依存関係のインストールやビルド処理を大幅にスキップできます。
依存関係キャッシュの設定
まず、node_modules や Yarn のキャッシュを保存することで、パッケージインストール時間を短縮できます。
GitHub Actions での設定例を見てみましょう。
yaml# GitHub Actions でのキャッシュ設定例
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Node.js のセットアップとキャッシュ
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
このように cache: 'yarn' を指定するだけで、yarn.lock に基づいた自動キャッシュが有効になります。
ビルド成果物のキャッシュ
次に、Vitest 自体のキャッシュディレクトリをキャッシュすることで、さらなる高速化が可能です。
yaml# Vitest キャッシュディレクトリの保存
- name: Cache Vitest
uses: actions/cache@v4
with:
path: |
node_modules/.vitest
node_modules/.vite
key: vitest-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
vitest-${{ runner.os }}-
node_modules/.vitest には、Vitest が生成した変換済みモジュールや依存関係グラフがキャッシュされています。これを再利用することで、初回実行時の解析処理をスキップできるのです。
キャッシュキーの最適化
キャッシュの効果を最大化するには、適切なキャッシュキーの設計が重要です。
yaml# 段階的なキャッシュキー設定
- uses: actions/cache@v4
with:
path: node_modules
key: deps-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
deps-${{ runner.os }}-
restore-keys を指定することで、完全一致するキャッシュがない場合でも、部分一致するキャッシュから復元できます。これにより、依存関係が少し変更された場合でも、大部分を再利用できるでしょう。
2 つの手法の組み合わせ
テストシャーディングとキャッシュヒット最大化を組み合わせることで、相乗効果が生まれます。
以下の図は、最適化後のフローを示しています。
mermaidflowchart TD
pr["PR作成"] --> ci_start["CI開始"]
ci_start --> cache_check["キャッシュ確認"]
cache_check -->|ヒット| restore["依存関係復元<br/>(10秒)"]
cache_check -->|ミス| install_new["インストール<br/>(60秒)"]
restore --> shard_split["シャード分割"]
install_new --> shard_split
shard_split --> p1["シャード1 実行"]
shard_split --> p2["シャード2 実行"]
shard_split --> p3["シャード3 実行"]
shard_split --> p4["シャード4 実行"]
p1 --> merge_results["結果集約"]
p2 --> merge_results
p3 --> merge_results
p4 --> merge_results
merge_results --> final{成功?}
final -->|成功| done["マージ<br/>(合計3分)"]
final -->|失敗| fix["修正"]
style done fill:#ccffcc
style restore fill:#ccffcc
この組み合わせにより、従来 20 分かかっていたテストが 3 分程度に短縮されるケースも珍しくありません。
具体例
GitHub Actions での完全な設定例
実際のプロジェクトで使える GitHub Actions の設定を見てみましょう。
ワークフローファイルの基本構造
まず、.github/workflows/test.yml ファイルを作成します。
yaml# .github/workflows/test.yml
name: Test Suite
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
# 同時実行制御
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
concurrency 設定により、同じブランチでの複数実行がキャンセルされ、リソースの無駄遣いを防げます。
シャーディング用のマトリックス戦略
次に、複数のシャードを並列実行するためのマトリックス戦略を定義します。
yamljobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
total_shards: [4]
name: Test (Shard ${{ matrix.shard }}/${{ matrix.total_shards }})
fail-fast: false により、一つのシャードが失敗しても他のシャードは実行を続けます。これにより、すべての失敗を一度に検出できるのです。
依存関係とキャッシュのセットアップ
依存関係のインストールとキャッシュ設定を行います。
yamlsteps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
--frozen-lockfile オプションにより、yarn.lock に記載されたバージョンで確実にインストールされます。
Vitest キャッシュの設定
Vitest の内部キャッシュを保存・復元する設定を追加します。
yaml- name: Cache Vitest
uses: actions/cache@v4
with:
path: |
node_modules/.vitest
node_modules/.vite
key: vitest-cache-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('vitest.config.ts') }}
restore-keys: |
vitest-cache-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-
vitest-cache-${{ runner.os }}-
vitest.config.ts もキーに含めることで、設定変更時に確実にキャッシュが更新されます。
シャーディングテストの実行
実際にシャーディングを使ってテストを実行します。
yaml- name: Run tests
run: |
yarn vitest run \
--shard=${{ matrix.shard }}/${{ matrix.total_shards }} \
--coverage \
--reporter=json \
--reporter=verbose
env:
CI: true
--reporter=json により、後でカバレッジレポートを統合しやすくなります。
テスト結果の集約とアップロード
各シャードのテスト結果をアーティファクトとしてアップロードします。
yaml- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.shard }}
path: |
coverage/
test-results.json
retention-days: 7
if: always() により、テストが失敗した場合でも結果がアップロードされます。
カバレッジレポートの統合
複数のシャードから生成されたカバレッジレポートを統合するジョブを追加しましょう。
yamlmerge-coverage:
runs-on: ubuntu-latest
needs: test
if: always()
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
カバレッジファイルのダウンロードと統合
各シャードの結果をダウンロードし、統合します。
yaml- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: ./coverage-artifacts
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Merge coverage reports
run: |
yarn vitest --merge-reports ./coverage-artifacts
--merge-reports オプションにより、複数のカバレッジレポートを統合できます。
カバレッジレポートのアップロード
統合されたカバレッジレポートを外部サービスにアップロードします。
yaml- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage/coverage-final.json
flags: vitest
fail_ci_if_error: true
これにより、PR 上でカバレッジの変化を視覚的に確認できるようになります。
パフォーマンスチューニング
さらなる最適化のために、Vitest の設定ファイルを調整しましょう。
vitest.config.ts の最適化設定
typescript// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
// シャーディング時の並列実行設定
pool: 'threads',
poolOptions: {
threads: {
singleThread: false,
// シャード内でも並列実行
minThreads: 1,
maxThreads: 4,
},
},
},
});
pool: 'threads' により、ワーカースレッドを使った高速な並列実行が可能になります。
ファイルシステムキャッシュの活用
typescriptexport default defineConfig({
test: {
// キャッシュディレクトリの明示的指定
cache: {
dir: 'node_modules/.vitest',
},
// 依存関係の解析をキャッシュ
deps: {
optimizer: {
web: {
enabled: true,
},
},
},
},
});
依存関係のオプティマイザーを有効にすることで、モジュール解析が大幅に高速化されます。
グローバルセットアップの最適化
typescriptexport default defineConfig({
test: {
// グローバルセットアップは1回だけ実行
globalSetup: './tests/setup.ts',
// 環境の再利用
isolate: false,
},
});
isolate: false により、テスト間で環境が再利用され、セットアップコストが削減されます。ただし、テスト間の独立性が重要な場合は true のままにしましょう。
実行時間の比較
最適化前後での実行時間を比較してみましょう。
| # | 構成 | テスト数 | 実行時間 | CI ランナー | コスト |
|---|---|---|---|---|---|
| 1 | 最適化前(単一実行) | 1000 | 20 分 | 1 台 | 基準 |
| 2 | 並列実行のみ | 1000 | 12 分 | 1 台 | 基準 |
| 3 | シャーディング(2 分割) | 1000 | 7 分 | 2 台 | 1.4 倍 |
| 4 | シャーディング(4 分割) | 1000 | 4 分 | 4 台 | 1.6 倍 |
| 5 | シャーディング+キャッシュ | 1000 | 3 分 | 4 台 | 2.4 倍 |
シャーディングとキャッシュを組み合わせることで、実行時間が約 85% 削減され、開発者の待ち時間が劇的に短縮されます。
GitLab CI での設定例
GitHub Actions 以外の CI サービスでも同様の最適化が可能です。GitLab CI の例を見てみましょう。
yaml# .gitlab-ci.yml
variables:
YARN_CACHE_FOLDER: .yarn-cache
# キャッシュ設定
cache:
key:
files:
- yarn.lock
paths:
- node_modules/
- .yarn-cache/
- node_modules/.vitest/
test:
stage: test
parallel: 4
script:
- yarn install --frozen-lockfile
- yarn vitest run --shard=${CI_NODE_INDEX}/${CI_NODE_TOTAL}
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
GitLab CI では parallel: 4 を指定するだけで、自動的に 4 つのジョブが並列実行されます。CI_NODE_INDEX と CI_NODE_TOTAL 変数を使うことで、シャード番号が自動的に割り当てられるのです。
まとめ
大規模プロジェクトでの Vitest 実行を高速化するには、テストシャーディングとキャッシュヒット最大化の 2 つの手法が効果的です。
テストシャーディングは、テストスイートを複数のグループに分割し、それぞれを別々の CI ランナーで並列実行する仕組みでした。Vitest の --shard オプションを使うことで、簡単に実装できます。
キャッシュヒット最大化では、依存関係や Vitest の内部キャッシュを保存・復元することで、セットアップ時間を大幅に短縮できました。適切なキャッシュキーの設計により、ヒット率を最大化することが重要です。
これら 2 つを組み合わせることで、従来 20 分かかっていたテストが 3 分程度に短縮され、開発者の生産性が大きく向上します。CI コストは若干増加しますが、開発者の時間コストを考えると十分に価値のある投資といえるでしょう。
ぜひ、あなたのプロジェクトでもこれらの最適化技術を試してみてください。テスト実行の待ち時間から解放され、より快適な開発体験が得られるはずです。
関連リンク
articleVitest 大規模 CI の最適化技術:テストシャーディング × キャッシュヒット最大化
articleVitest テストデータ設計技術:Factory / Builder / Fixture の責務分離と再利用
articleVitest Matchers 技術チートシート:`expect.extend` で表現力を拡張する定石
articleVitest モノレポ技術セットアップ:pnpm / Nx / Turborepo で超高速化する手順
articleVitest モック技術比較:MSW / `vi.mock` / 手動スタブ — API テストの最適解はどれ?
articleVitest ESM/CJS 混在で `Cannot use import statement outside a module` が出る技術対処集
articleWebSocket Close コード早見表:正常終了・プロトコル違反・ポリシー違反の実務対応
articleStorybook 品質ゲート運用:Lighthouse/A11y/ビジュアル差分を PR で自動承認
articleWebRTC で高精細 1080p/4K 画面共有:contentHint「detail」と DPI 最適化
articleSolidJS フォーム設計の最適解:コントロール vs アンコントロールドの棲み分け
articleWebLLM 使い方入門:チャット UI を 100 行で実装するハンズオン
articleShell Script と Ansible/Make/Taskfile の比較:小規模自動化の最適解を検証
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来