LESSON 40分

「パフォーマンスの回帰は、機能のバグと同じように扱うべきだ」と佐藤CTOは言った。「CI/CDパイプラインにパフォーマンスチェックを組み込めば、デプロイ前に問題を検知できる。本番で気づくのでは遅い。」

1. パフォーマンス回帰テストの自動化

# .github/workflows/performance.yml
name: Performance Regression Test

on:
  pull_request:
    branches: [main]

jobs:
  performance:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_DB: testdb
          POSTGRES_PASSWORD: test
        ports: ['5432:5432']
      redis:
        image: redis:7
        ports: ['6379:6379']

    steps:
      - uses: actions/checkout@v4

      - name: Setup & Start App
        run: |
          npm ci
          npm run build
          npm start &
          sleep 10

      - name: Run k6 Performance Test
        uses: grafana/k6-action@v0.3
        with:
          filename: tests/performance/smoke.js
          flags: --out json=results.json

      - name: Check Performance Budget
        run: |
          node scripts/check-performance-budget.js results.json

      - name: Comment PR with Results
        if: always()
        uses: actions/github-script@v7
        with:
          script: |
            const results = require('./results-summary.json');
            const body = `## Performance Test Results
            | Metric | Value | Budget | Status |
            |--------|-------|--------|--------|
            | p95 Latency | ${results.p95}ms | <500ms | ${results.p95 < 500 ? 'PASS' : 'FAIL'} |
            | p99 Latency | ${results.p99}ms | <1000ms | ${results.p99 < 1000 ? 'PASS' : 'FAIL'} |
            | Error Rate | ${results.errorRate}% | <1% | ${results.errorRate < 1 ? 'PASS' : 'FAIL'} |
            | RPS | ${results.rps} | >100 | ${results.rps > 100 ? 'PASS' : 'FAIL'} |`;
            github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body
            });

2. Lighthouse CI

// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: ['http://localhost:3000/', 'http://localhost:3000/products'],
      numberOfRuns: 3,
      settings: {
        preset: 'desktop',
        throttling: { cpuSlowdownMultiplier: 1 },
      },
    },
    assert: {
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'first-contentful-paint': ['warn', { maxNumericValue: 1500 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'total-blocking-time': ['error', { maxNumericValue: 300 }],
        'interactive': ['warn', { maxNumericValue: 3500 }],
      },
    },
    upload: {
      target: 'temporary-public-storage',
    },
  },
};
# GitHub Actions での Lighthouse CI
- name: Lighthouse CI
  uses: treosh/lighthouse-ci-action@v11
  with:
    configPath: './lighthouserc.js'
    uploadArtifacts: true
    temporaryPublicStorage: true

3. パフォーマンスバジェットのCI統合

// scripts/check-performance-budget.ts
interface PerformanceBudget {
  metric: string;
  budget: number;
  unit: string;
  severity: 'error' | 'warning';
}

const budgets: PerformanceBudget[] = [
  { metric: 'http_req_duration_p95', budget: 500, unit: 'ms', severity: 'error' },
  { metric: 'http_req_duration_p99', budget: 1000, unit: 'ms', severity: 'error' },
  { metric: 'http_req_failed_rate', budget: 0.01, unit: '%', severity: 'error' },
  { metric: 'bundle_size_main', budget: 250, unit: 'KB', severity: 'warning' },
  { metric: 'bundle_size_vendor', budget: 500, unit: 'KB', severity: 'warning' },
];

function checkBudgets(
  results: Record<string, number>,
  budgets: PerformanceBudget[]
): { passed: boolean; violations: string[] } {
  const violations: string[] = [];

  for (const budget of budgets) {
    const actual = results[budget.metric];
    if (actual !== undefined && actual > budget.budget) {
      violations.push(
        `[${budget.severity.toUpperCase()}] ${budget.metric}: ${actual}${budget.unit} > budget ${budget.budget}${budget.unit}`
      );
    }
  }

  const hasErrors = violations.some(v => v.startsWith('[ERROR]'));
  return { passed: !hasErrors, violations };
}

バンドルサイズの監視

// webpack-bundle-analyzer or size-limit
// .size-limit.json
[
  { "path": "dist/main.*.js", "limit": "250 KB", "gzip": true },
  { "path": "dist/vendor.*.js", "limit": "500 KB", "gzip": true },
  { "path": "dist/**/*.css", "limit": "50 KB", "gzip": true }
]

4. 本番パフォーマンスモニタリング

// パフォーマンスダッシュボードの設計
interface PerformanceDashboard {
  realTimeMetrics: string[];
  alertRules: AlertRule[];
  sloTargets: SloTarget[];
}

interface AlertRule {
  name: string;
  condition: string;
  threshold: number;
  window: string;
  severity: 'critical' | 'warning';
}

interface SloTarget {
  name: string;
  target: number;  // 例: 99.9%
  indicator: string;
}

const dashboard: PerformanceDashboard = {
  realTimeMetrics: [
    'request_rate', 'error_rate',
    'latency_p50', 'latency_p95', 'latency_p99',
    'cpu_usage', 'memory_usage', 'active_connections',
  ],
  alertRules: [
    { name: 'High Latency', condition: 'p95 > threshold', threshold: 500,
      window: '5m', severity: 'warning' },
    { name: 'Error Spike', condition: 'error_rate > threshold', threshold: 5,
      window: '2m', severity: 'critical' },
    { name: 'Saturation', condition: 'cpu_usage > threshold', threshold: 85,
      window: '10m', severity: 'warning' },
  ],
  sloTargets: [
    { name: 'Availability', target: 99.9, indicator: 'successful_requests / total_requests' },
    { name: 'Latency', target: 99, indicator: 'requests_under_500ms / total_requests' },
  ],
};

Canary デプロイでのパフォーマンス比較

// Canary vs Baseline のパフォーマンス比較
interface CanaryAnalysis {
  metric: string;
  baseline: number;
  canary: number;
  degradationPercent: number;
  verdict: 'pass' | 'fail' | 'inconclusive';
}

function analyzeCanary(
  baselineMetrics: Record<string, number>,
  canaryMetrics: Record<string, number>,
  maxDegradation: number = 10  // 10%以上の劣化でfail
): CanaryAnalysis[] {
  return Object.keys(baselineMetrics).map(metric => {
    const baseline = baselineMetrics[metric];
    const canary = canaryMetrics[metric];
    const degradationPercent = ((canary - baseline) / baseline) * 100;

    return {
      metric,
      baseline,
      canary,
      degradationPercent: Math.round(degradationPercent * 10) / 10,
      verdict: degradationPercent > maxDegradation ? 'fail' :
               degradationPercent > maxDegradation / 2 ? 'inconclusive' : 'pass',
    };
  });
}

まとめ

トピック要点
CI統合PR ごとに負荷テストを自動実行し回帰を検知
Lighthouse CICore Web Vitals をCIでチェック、スコア基準で合否判定
バジェット管理バンドルサイズ・レイテンシにバジェットを設定しCIで監視
本番モニタリングSLO ベースのアラート、Canary デプロイでのA/B比較

チェックリスト

  • CI/CDにパフォーマンステストを組み込める
  • Lighthouse CI でフロントエンド品質を自動チェックできる
  • パフォーマンスバジェットを定義し監視できる
  • Canary デプロイでパフォーマンス回帰を検知できる

次のステップへ

パフォーマンスCI/CDを学んだ。次は 演習 で、実際に負荷テストの計画を設計してみよう。

推定読了時間: 40分