「パフォーマンスの回帰は、機能のバグと同じように扱うべきだ」と佐藤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 CI | Core Web Vitals をCIでチェック、スコア基準で合否判定 |
| バジェット管理 | バンドルサイズ・レイテンシにバジェットを設定しCIで監視 |
| 本番モニタリング | SLO ベースのアラート、Canary デプロイでのA/B比較 |
チェックリスト
- CI/CDにパフォーマンステストを組み込める
- Lighthouse CI でフロントエンド品質を自動チェックできる
- パフォーマンスバジェットを定義し監視できる
- Canary デプロイでパフォーマンス回帰を検知できる
次のステップへ
パフォーマンスCI/CDを学んだ。次は 演習 で、実際に負荷テストの計画を設計してみよう。
推定読了時間: 40分