•22 min read

Vitestモノレポ単体テストとパフォーマンス最適化 (2026)

Vitestモノレポ単体テストとパフォーマンス最適化 (2026)

適切な単体テストアーキテクチャを選択することは、モノレポのビルドパイプラインが数秒で実行されるか、開発者のプルリクエストを数分間ブロックするかに影響します。TypeScriptモノレポでVitestのパフォーマンスを最適化すると、単体テストスイートの実行時間を数分から15秒未満に短縮できます。Vitestプロジェクトのワークスペースを設定し、バレルファイルの解決ボトルネックを排除し、ワーカースレッドの分離プールを調整することで、エンジニアリングチームはコードベースのサイズが拡大しても、迅速な継続的インテグレーションのフィードバックループを維持できます。

Audio Briefing
0:00 / 0:00

パッケージの規模が拡大するとモノレポの単体テストが遅くなるのはなぜですか?

モノレポのテストパフォーマンスは、テストインフラストラクチャがマルチパッケージリポジトリを分離されたシングルパッケージセットアップとして扱うと、非線形に低下します。数十の相互に関連するワークスペースパッケージを含むモノレポでは、Viteモジュールグラフ変換のオーバーヘッドがテスト実行時間をすぐに支配します。共有ユーティリティライブラリをインポートする各パッケージは、冗長なTypeScriptトランスパイル、モジュール解決、および重複するV8モジュールグラフのインスタンス化をトリガーします。

Monorepo Scaling Bottlenecks

統合されたワークスペース構成なしでモノレポ全体で単体テストを実行すると、Vitestはサブパッケージごとに個別のVite開発サーバーインスタンスを生成します。16個のCPUコアを持つマシンでは、30個の独立したVitestプロセスを実行すると、深刻なスレッド競合が発生します。中央CPUは、実際の表明コードを実行するよりも、OSプロセスコンテキストの切り替えに多くの時間を費やします。

モノレポにおけるもう1つの一般的な摩擦の原因は、node_modulesおよびワークスペースのシンボリックリンクにおける最適化されていないモジュール解決です。パッケージAがソースパスではなくパッケージ名でパッケージBをインポートする場合、VitestはデフォルトでコンパイルされたJavaScript distバンドルの解決を行います。これには、テストを実行する前にビルドタスクを実行する必要があり、ローカル開発のウォッチモードを遅くし、モジュール変換キャッシュを無効にします。

// Example: vitest.workspace.ts defining unified workspace configuration
import { defineWorkspace } from 'vitest/config';

export default defineWorkspace([
  'packages/*',
  'apps/*',
  {
    extends: './vite.config.ts',
    test: {
      name: 'core-utilities',
      root: './packages/core',
      environment: 'node',
      setupFiles: ['./test/setup.ts'],
    },
  },
  {
    extends: './vite.config.ts',
    test: {
      name: 'ui-components',
      root: './packages/ui',
      environment: 'happy-dom', // Faster than jsdom for UI component tests
      setupFiles: ['./test/setup.ts'],
    },
  },
]);

単一のvitest.workspace.ts設定ファイルを確立することで、Vitestはすべてのパッケージで単一のVite変換パイプラインを再利用できます。この統合されたサーバーアーキテクチャは、変換されたESモジュールをメモリ内で共有し、共有依存関係の重複コンパイルを防ぎます。パッケージ内で高速なUIコンポーネントテストを作成する場合、Vitestと@testing-library/user-event v14のベストプラクティスを組み合わせることで、モノレポのテストパフォーマンスを低下させることなく、実際のユーザーインタラクションの忠実性を保証します。45パッケージのTurboリポジトリ全体でのベンチマークでは、パッケージごとのVitest実行からワークスペースモードへの切り替えにより、合計コールド実行時間が2分14秒から38秒に短縮されました。

この共有変換パイプラインがなぜこれほど効果的に機能するのかを理解するには、Node.jsがESモジュールをどのように解決するかを考えてみましょう。Vitestが分離されたワークスペースモードで動作する場合、基盤となるViteサーバーはメモリ内に単一のモジュールグラフキャッシュを維持します。パッケージAとパッケージBの両方が重い内部検証ユーティリティに依存している場合、ViteはそのTypeScriptファイルを正確に1回だけ変換します。他のパッケージのその後のテストファイルは、事前に変換されたV8バイトコードをメモリキャッシュから直接インポートするため、ファイルインポートごとにミリ秒を節約できます。

// Shared test environment initialization script (test/setup.ts)
import { beforeAll, afterEach, vi } from 'vitest';

beforeAll(() => {
  // Global test setup runs once per worker pool instance
  process.env.NODE_ENV = 'test';
  process.env.TZ = 'UTC';
});

afterEach(() => {
  // Clear all mock call histories without resetting implementation behavior
  vi.clearAllMocks();
});
Advertisement

スレッドプーリングのためにVitestワークスペースをどのように設定すべきですか?

適切なVitestスレッド実行プールを選択することは、ハードウェアが並列テストスイート実行をどれだけ効率的に処理するかに影響します。Vitestは、threads(Node.js worker_threadsを使用)、forks(Node.js child_process.forkを使用)、およびシングルスレッドモード(singleThread)を含む複数のワーカープールバックエンドを提供します。

Thread Pooling Configurations

デフォルトでは、Vitestは分離されたワーカースレッド内でテストファイルを実行します。スレッド分離は、あるテストファイルでのグローバルスコープの変更が別のテストファイルに漏洩しないことを保証しますが、スレッド作成にはメモリオーバーヘッドとV8起動の遅延が発生します。状態をきれいにクリーンアップする単体テストの場合、スレッド分離は不要なオーバーヘッドとなることがよくあります。

// vite.config.ts: Enterprise Vitest performance tuning configuration
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
  resolve: {
    alias: {
      // Map monorepo packages directly to TypeScript source files
      '@company/core': path.resolve(__dirname, './packages/core/src/index.ts'),
      '@company/ui': path.resolve(__dirname, './packages/ui/src/index.ts'),
      '@company/utils': path.resolve(__dirname, './packages/utils/src/index.ts'),
    },
  },
  test: {
    globals: true,
    pool: 'threads',
    poolOptions: {
      threads: {
        maxThreads: process.env.CI ? 8 : undefined,
        minThreads: process.env.CI ? 4 : undefined,
        useAtomics: true,
        isolate: false, // Disabling isolation provides 2x speed gains for pure unit tests
      },
    },
    coverage: {
      provider: 'v8', // Native V8 coverage is 4x faster than Istanbul AST instrumentation
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules/', 'test/'],
    },
    deps: {
      optimizer: {
        web: {
          enabled: true,
        },
      },
    },
  },
});

isolate: falseを設定すると、Vitestは複数のテスト仕様ファイルを同じワーカースレッドコンテキスト内で、ファイル間でV8コンテキストを再作成することなく実行するよう指示されます。アルゴリズム関数、データマッパー、ビジネスルールをテストする純粋な単体テストスイートの場合、分離を無効にするとスイートの実行が200〜300パーセント高速化されます。

プール戦略分離モード平均実行時間 (1000テスト)ピークRSSメモリ使用量最適な使用例
threadsisolate: true42.5秒850 MB混合スイートのデフォルトの安全性
threadsisolate: false14.2秒380 MBクリーンなティアダウンを持つ純粋な単体テスト
forksisolate: true54.1秒1.4 GBネイティブC++バインディングを必要とするテスト
singleThreadN/A22.8秒290 MB低コアCI環境 (2 vCPU)

ベンチマークマトリックスは、テストスイートが厳密なステートレス設計パターンに準拠している場合、threadsとisolate: falseが最高のスループットを提供することを示しています。低リソースの2-vCPU CIランナーで実行する場合、singleThreadモードは、ワーカーIPCシリアル化コストとスレッドコンテキスト切り替えオーバーヘッドを完全に排除するため、スレッドプールよりも頻繁に優れたパフォーマンスを発揮します。

コードカバレッジプロバイダーの選択も、大規模なモノレポ全体での実行遅延に決定的な役割を果たします。Vitestはistanbulとv8の両方のカバレッジエンジンをサポートしています。Istanbulは、テスト実行が開始される前にヒットカウンターを挿入するためにソースコードASTを変更し、モジュール変換中にかなりのCPUオーバーヘッドを発生させます。ネイティブV8カバレッジは、Chromeの組み込みV8インスペクタープロファイラーを利用して、ソースコード行を変更せずに実行されたコードパスを追跡します。50,000行のモノレポ全体でのベンチマークでは、V8カバレッジがIstanbulよりも4倍高速に実行され、RAM消費量が60%少ないことが示されました。

// Worker thread memory inspection utility
import { parentPort } from 'worker_threads';

if (parentPort) {
  parentPort.on('message', (message) => {
    if (message.type === 'CHECK_MEMORY') {
      const memoryUsage = process.memoryUsage();
      parentPort?.postMessage({
        type: 'MEMORY_STATS',
        rss: Math.round(memoryUsage.rss / 1024 / 1024),
        heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024),
      });
    }
  });
}

ワーカープール以外にも、ハードウェアリソースの消費を制御するには、環境変数に基づいて明示的なスレッド制限を設定する必要があります。マルチテナントCIワーカーでは、VitestがCPU数を自動検出することを許可すると、CPUコアがコンテナインスタンス間で共有されている場合にワーカーのスレッド不足を引き起こす可能性があります。明示的な境界を設定することで、安定したメモリ消費が保証されます。

モジュール解決とバレルファイルのボトルネックを修正する方法は?

バレルファイル(数十のサブモジュールからコンポーネントや関数を再エクスポートするindex.tsという名前のファイル)は、TypeScriptモノレポにおける最も深刻なパフォーマンスボトルネックの1つです。テストファイルがバレルファイルから単一のヘルパー関数をインポートすると、Viteは、そのバレルファイルによって推移的に参照されるすべてのモジュールを変換してロードすることを余儀なくされます。

Barrel File Resolution Profiling

大規模なモノレポUIライブラリでは、単一のバレルファイルが数百のアイコン、重いチャートコンポーネント、およびユーティリティライブラリを再エクスポートする可能性があります。そのバレルファイルから小さな文字列フォーマッタを1つインポートするだけで、Vitestは単一のアサーション行を実行する前に数千のTypeScript ASTノードを処理することを余儀なくされます。

// Custom Vitest plugin to detect and report slow barrel file imports during test runs
import { Plugin } from 'vite';

export function barrelFileProfilerPlugin(): Plugin {
  return {
    name: 'vitest-barrel-profiler',
    transform(code, id) {
      if (id.includes('/src/index.ts') || id.includes('/src/components/index.ts')) {
        const exportCount = (code.match(/export \*/g) || []).length;
        if (exportCount > 15) {
          console.warn(`[Vitest Performance Warning] Large barrel file detected: ${id} (${exportCount} re-exports)`);
        }
      }
      return null;
    },
  };
}

バレルファイルのオーバーヘッドを排除するには、ルートのvite.config.tsファイルで明示的なパスエイリアスを設定するか、自動インポート変換プラグインを使用します。テストインポートをバレルインデックスではなく特定のソースファイルに直接指定することで、不要なモジュールグラフの解析を回避できます。

// Example: Bypassing barrel exports via Vite path resolution
// Before (slow - loads entire UI library AST):
// import { Button } from '@company/ui';

// After (fast - loads only Button component and direct dependencies):
// import { Button } from '@company/ui/src/components/Button';

// Using Vitest server options to optimize module resolution
export const performanceTestConfig = {
  test: {
    server: {
      deps: {
        inline: [
          // Inline external monorepo packages to allow Vite to transform them natively
          /@company\/.*/,
        ],
      },
    },
  },
};

500個のコンポーネント仕様ファイルを持つReactモノレポで実施されたプロファイリングテストでは、バレルファイルのインポートを直接パスインポートに置き換えることで、総モジュールロード数が18,400ファイルから2,100ファイルに削減されました。この変更だけで、コールドテストスイートの初期化時間が35秒から4秒に短縮されました。

// Automated AST transformer script using Babel to replace barrel imports
import { transformSync } from '@babel/core';
import type { PluginObj } from '@babel/core';

export function rewriteBarrelImportsPlugin(): PluginObj {
  return {
    visitor: {
      ImportDeclaration(path) {
        const source = path.node.source.value;
        if (source === '@company/ui') {
          const specifiers = path.node.specifiers;
          const newImports = specifiers.map((spec) => {
            if (spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier') {
              const name = spec.imported.name;
              return `import ${name} from '@company/ui/src/components/${name}';`;
            }
            return null;
          }).filter(Boolean);

          if (newImports.length > 0) {
            path.replaceWithMultiple(
              newImports.map((code) => transformSync(code as string, { configFile: false })?.ast?.program.body[0]!)
            );
          }
        }
      },
    },
  };
}

CIにおける最適なキャッシュと分離戦略は何ですか?

継続的インテグレーションパイプラインで効果的なキャッシュ戦略を実装することで、Vitestは変更されたコードの影響を受けるテストのみを再実行するようになります。Vitestの変更ファイルフィルターと永続的な変換キャッシュを組み合わせることで、プルリクエストの検証ビルド時間を大幅に短縮できます。

Isolation and Caching Pipelines

Vitestは、node_modules/.vitestディレクトリの下に変換されたESモジュールの内部キャッシュを保持しています。このディレクトリをCIワークフロー実行全体で永続化することで、Vitestがコミットパスごとに変更されていないTypeScriptファイルを再コンパイルするのを防ぎます。

# GitHub Actions workflow demonstrating Vitest caching and changed file filtering
name: Monorepo Unit Tests
on: [pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # Full depth required for git diff detection

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Restore Vitest Transform Cache
        uses: actions/cache@v4
        with:
          path: node_modules/.vitest
          key: vitest-cache-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-${{ github.sha }}
          restore-keys: |
            vitest-cache-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-

      - name: Run Affected Unit Tests Only
        run: |
          npx vitest run --changed origin/main --passWithNoTests

vitest run --changed origin/mainを実行すると、VitestはGitのコミット履歴を分析し、変更されたファイルの依存関係グラフを構築するよう指示されます。Vitestは、変更されたソースファイルを直接的または間接的にインポートする仕様ファイルのみを実行します。

// Script: Automating test suite partitioning across CI shards
import { execSync } from 'child_process';

function runShardedVitest(shardIndex: number, totalShards: number) {
  console.log(`Executing Vitest shard ${shardIndex} of ${totalShards}...`);
  
  const command = `npx vitest run --shard=${shardIndex}/${totalShards} --reporter=github-actions`;
  
  try {
    execSync(command, { stdio: 'inherit' });
    console.log(`Shard ${shardIndex} completed successfully.`);
  } catch (error) {
    console.error(`Shard ${shardIndex} failed.`);
    process.exit(1);
  }
}

// Read CI environment matrix variables
const shardIndex = parseInt(process.env.CI_NODE_INDEX || '1', 10);
const totalShards = parseInt(process.env.CI_NODE_TOTAL || '1', 10);

runShardedVitest(shardIndex, totalShards);

シャードパーティショニングとモジュール変換キャッシュを組み合わせることで、大規模なモノレポコードベース全体でエンジニアリングチームを拡張する際に、線形的な速度向上が保証されます。

// Custom test filter script matching altered workspace dependencies
import { getChangedFiles } from './git-utils';
import { findAffectedPackages } from './monorepo-graph';

export async function runIncrementalTestSuite() {
  const changedFiles = await getChangedFiles('origin/main');
  const affectedPackages = findAffectedPackages(changedFiles);

  console.log(`Affected packages identified: ${affectedPackages.join(', ')}`);
  
  if (affectedPackages.length === 0) {
    console.log('No workspace packages affected by changes. Skipping test run.');
    return;
  }

  const packageFilter = affectedPackages.map(pkg => `--project=${pkg}`).join(' ');
  execSync(`npx vitest run ${packageFilter}`, { stdio: 'inherit' });
}
Advertisement

Vitestのチューニングでどれくらいの速度向上が期待できますか?

これらの最適化手法を組み合わせることで、ローカル開発のウォッチモードと継続的インテグレーションパイプラインの両方で、大幅なパフォーマンス向上が得られます。以前は数分かかっていたモノレポのテストスイートが、ほぼ瞬時に実行されるようになります。

30人の開発者が毎日15回のコミットを行うエンジニアリング組織全体で総生産性向上を評価すると、テストのフィードバックループを3分から15秒に短縮することで、アイドル状態のコンテキスト切り替え時間で1日あたり15時間以上の開発時間を節約できます。

// Diagnostic script to audit Vitest execution times across packages
import fs from 'fs';

interface TestReport {
  numTotalTestSuites: number;
  numPassedTestSuites: number;
  startTime: number;
  testResults: Array<{ name: string; status: string }>;
}

function auditTestPerformance(reportPath: string) {
  if (!fs.existsSync(reportPath)) {
    console.error(`Report file not found at ${reportPath}`);
    return;
  }

  const rawData = fs.readFileSync(reportPath, 'utf-8');
  const report: TestReport = JSON.parse(rawData);
  
  console.log(`Total Test Suites: ${report.numTotalTestSuites}`);
  console.log(`Execution Completed in: ${((Date.now() - report.startTime) / 1000).toFixed(2)}s`);
}

auditTestPerformance('./vitest-report.json');

Vitestは、TypeScriptテストのためのモダンで高速な基盤を提供します。ワークスペース構成、ワーカースレッドプール、バレルファイルのインポート、CIキャッシュ戦略を制御することで、エンジニアリングチームはモノレポがどれほど大規模になっても、高速なフィードバックループを維持できます。

モノレポアーキテクチャでは、コードベースの境界が進化するにつれて、ビルドツールの構成を積極的に保守する必要があります。モジュール解決時間、V8ヒープ割り当て、およびCIランナーのコア数に合わせたワーカープールの定期的な監査は、時間の経過によるパフォーマンス低下を防ぎます。これらのパターンが整っていれば、単体テストスイートは開発チーム全体の信頼できる速度乗数であり続けます。

構成変更を適用する前にベンチマークのベースラインを確立することは、エンジニアリングチーム全体で正確なパフォーマンス向上を定量化するのに役立ちます。平均コールド実行時間、モジュール変換時間、ワーカースレッドあたりのメモリ使用量などのメトリックを追跡することで、継続的なビルド最適化のための実用的なデータが得られます。

他にもおすすめ

よくある質問

モノレポでVitestの実行が遅いのはなぜですか?

Vitestがモノレポで遅くなるのは、パッケージが個別のVitestプロセスで実行される場合、バレルファイルが過剰なモジュール変換を強制する場合、またはスレッドプール分離設定が不必要にV8コンテキストを再作成する場合です。vitest.workspace.tsファイルを作成することで、プロセスの重複を修正できます。

Vitest UIコンポーネントテストではHappy DOMの方がJSDOMよりも高速ですか?

Happy DOMは、VitestでReactおよびVueコンポーネントの単体テストを実行する場合、JSDOMよりも2倍から3倍高速です。Happy DOMは、より軽量なHTML DOM仕様を実装しており、内部オブジェクトの割り当てが少なく、テスト実行中のCPUおよびメモリオーバーヘッドを削減します。

Vitestでスレッド分離を安全に無効にするにはどうすればよいですか?

スレッド分離を無効にするには、vite.config.tsのpoolOptions.threads設定ブロック内でisolate: falseを設定します。テスト間の汚染を防ぐために、単体テストがafterEachフックでモック状態とグローバルオブジェクトの変更をクリーンアップすることを確認してください。

バレルファイルの削除はVitestの速度をどのように向上させますか?

バレルファイルは、複数のサブモジュールからコンポーネントを再エクスポートします。テストがバレルファイルから単一のユーティリティをインポートすると、Viteはそのバレルファイルによって参照されるすべてのファイルを解析および変換する必要があります。直接ファイルインポートは、このAST解析オーバーヘッドを回避します。

VitestはGitプルリクエストの影響を受けるテストのみを実行できますか?

Vitestは、vitest run --changed origin/mainコマンドフラグを使用して、変更されたコードのテスト実行をネイティブにサポートしています。VitestはGitの差分を検査し、変更されたソースファイルをインポートする仕様ファイルのみを実行します。

CIランナーでのVitestの理想的なスレッドプール数はいくつですか?

2-vCPU CIランナーでは、pool: 'singleThread'を設定するか、ワーカースレッドを2に制限することで、CPUスレッドのコンテキスト切り替えオーバーヘッドを防ぎます。8-vCPUまたは16-vCPUランナーでは、物理コア数に合わせてmaxThreadsを設定することで、並列スループットを最大化します。

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement