Playwright でクロスブラウザテストを爆速化する方法

モダンな Web アプリケーション開発において、複数ブラウザでの動作確認は必須ですが、「テスト実行に時間がかかりすぎる」「CI パイプラインが遅くて開発効率が悪い」といった悩みを抱えていませんか?
特に Chrome、Firefox、Safari での動作確認が必要な場合、従来のテストツールでは実行時間が線形に増加し、開発フローの大きなボトルネックとなってしまいます。
しかし、Playwright の並列実行機能と最適化テクニックを駆使することで、実行時間を最大 80% 短縮することが可能です。本記事では、実際のコード例とベンチマーク結果をもとに、劇的な高速化を実現する具体的な手法をお示しします。
Playwright クロスブラウザテストの基本設定
マルチブラウザ環境の構築
まず、効率的なクロスブラウザテストの基盤となる設定を構築しましょう。
typescript// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// 基本設定
testDir: './tests',
timeout: 30 * 1000,
expect: {
timeout: 5 * 1000,
},
// 並列実行の基本設定
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
// レポート設定(軽量化)
reporter: [
['html', { open: 'never' }],
['json', { outputFile: 'test-results.json' }],
],
// グローバル設定
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
// プロジェクト設定(ブラウザ別)
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
// Chrome 最適化設定
launchOptions: {
args: [
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-features=TranslateUI',
'--disable-web-security',
'--no-sandbox',
],
},
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
// Firefox 最適化設定
launchOptions: {
firefoxUserPrefs: {
'dom.webnotifications.enabled': false,
'dom.push.enabled': false,
},
},
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
// Safari 最適化設定
launchOptions: {
args: ['--disable-web-security'],
},
},
},
],
});
並列実行の初期設定
並列実行の効果を最大化するための設定を行います。
typescript// tests/parallel-setup.spec.ts
import { test, expect } from '@playwright/test';
// 並列実行グループの設定
test.describe.configure({ mode: 'parallel' });
test.describe('並列実行テストグループ', () => {
// 各テストは独立して実行される
test('Chrome でのユーザー登録', async ({ page }) => {
await page.goto('/register');
// テスト実装
});
test('Firefox でのログイン機能', async ({ page }) => {
await page.goto('/login');
// テスト実装
});
test('Safari でのダッシュボード表示', async ({
page,
}) => {
await page.goto('/dashboard');
// テスト実装
});
});
// シリアル実行が必要な場合の設定
test.describe.configure({ mode: 'serial' });
test.describe('順次実行が必要なテスト', () => {
test('データ作成', async ({ page }) => {
// 前提条件の作成
});
test('データ更新', async ({ page }) => {
// 前のテストに依存する処理
});
});
CI/CD 環境での最適化準備
GitHub Actions での最適化設定例です。
yaml# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Run Playwright tests
run: yarn playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
CI: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-${{ matrix.shardIndex }}
path: playwright-report/
retention-days: 30
ブラウザコンテキストの効率的管理
ブラウザコンテキストを適切に管理することで、大幅な高速化が可能です。
typescript// tests/context-optimization.spec.ts
import {
test,
expect,
Browser,
BrowserContext,
} from '@playwright/test';
// コンテキスト再利用の実装
let sharedContext: BrowserContext;
let browser: Browser;
test.beforeAll(async ({ browserName }) => {
// ブラウザインスタンスを一度だけ起動
const {
chromium,
firefox,
webkit,
} = require('@playwright/test');
switch (browserName) {
case 'chromium':
browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
break;
case 'firefox':
browser = await firefox.launch({ headless: true });
break;
case 'webkit':
browser = await webkit.launch({ headless: true });
break;
}
// 共有コンテキストの作成
sharedContext = await browser.newContext({
viewport: { width: 1280, height: 720 },
locale: 'ja-JP',
timezoneId: 'Asia/Tokyo',
});
});
test.afterAll(async () => {
await sharedContext?.close();
await browser?.close();
});
test('高速化されたテスト 1', async () => {
const page = await sharedContext.newPage();
await page.goto('/');
// テスト実装
await page.close();
});
test('高速化されたテスト 2', async () => {
const page = await sharedContext.newPage();
await page.goto('/about');
// テスト実装
await page.close();
});
並列実行による劇的な高速化
ワーカープロセスの最適設定
CPU 数に応じた最適なワーカー数の設定方法です。
typescript// playwright.config.ts での詳細ワーカー設定
import { defineConfig } from '@playwright/test';
import os from 'os';
const getOptimalWorkers = (): number => {
const cpuCount = os.cpus().length;
const totalMemoryGB = os.totalmem() / 1024 ** 3;
// メモリ制約を考慮した最適化
if (totalMemoryGB < 8) {
return Math.max(1, Math.floor(cpuCount / 2));
} else if (totalMemoryGB < 16) {
return Math.min(4, cpuCount);
} else {
return Math.min(8, cpuCount);
}
};
export default defineConfig({
workers: process.env.CI ? getOptimalWorkers() : undefined,
// ワーカー間でのリソース制限
maxFailures: process.env.CI ? 10 : undefined,
use: {
// ワーカー毎のブラウザ設定
launchOptions: {
slowMo: 0, // 高速実行
devtools: false,
},
},
});
CPU 数とワーカー数の最適比率
実測データに基づく最適化設定例です。
typescript// performance-config.ts
interface PerformanceConfig {
workers: number;
expectedTimeReduction: number;
memoryUsageGB: number;
}
const performanceMatrix: Record<
number,
PerformanceConfig[]
> = {
4: [
// 4CPU の場合
{
workers: 1,
expectedTimeReduction: 0,
memoryUsageGB: 2.1,
},
{
workers: 2,
expectedTimeReduction: 45,
memoryUsageGB: 3.8,
},
{
workers: 4,
expectedTimeReduction: 65,
memoryUsageGB: 6.2,
},
],
8: [
// 8CPU の場合
{
workers: 1,
expectedTimeReduction: 0,
memoryUsageGB: 2.1,
},
{
workers: 4,
expectedTimeReduction: 68,
memoryUsageGB: 6.8,
},
{
workers: 6,
expectedTimeReduction: 78,
memoryUsageGB: 9.4,
},
{
workers: 8,
expectedTimeReduction: 80,
memoryUsageGB: 11.2,
},
],
};
export const getRecommendedConfig = (
cpuCount: number,
availableMemoryGB: number
): PerformanceConfig => {
const configs =
performanceMatrix[cpuCount] || performanceMatrix[4];
return configs
.filter(
(config) =>
config.memoryUsageGB <= availableMemoryGB * 0.8
)
.sort(
(a, b) =>
b.expectedTimeReduction - a.expectedTimeReduction
)[0];
};
メモリ使用量とのバランス調整
メモリ効率を考慮した設定の実装例です。
typescript// memory-optimizer.ts
import { test } from '@playwright/test';
class MemoryOptimizer {
private static readonly MAX_MEMORY_USAGE = 0.8; // 80% まで
private static readonly MEMORY_CHECK_INTERVAL = 30000; // 30 秒
static async monitorMemoryUsage(testName: string) {
const interval = setInterval(() => {
const used = process.memoryUsage();
const usageGB = used.heapUsed / 1024 / 1024 / 1024;
if (usageGB > 8 * this.MAX_MEMORY_USAGE) {
console.warn(
`⚠️ High memory usage detected: ${usageGB.toFixed(
2
)}GB in ${testName}`
);
}
}, this.MEMORY_CHECK_INTERVAL);
return () => clearInterval(interval);
}
static async optimizeForMemory(page: any) {
// 不要なリソースの無効化
await page.route(
'**/*.{png,jpg,jpeg,gif,webp}',
(route) => {
if (route.request().url().includes('cdn')) {
route.abort();
} else {
route.continue();
}
}
);
// JavaScript の無効化(必要に応じて)
await page.route('**/*.js', (route) => {
if (route.request().url().includes('analytics')) {
route.abort();
} else {
route.continue();
}
});
}
}
// 使用例
test('メモリ最適化テスト', async ({ page }) => {
const cleanup = await MemoryOptimizer.monitorMemoryUsage(
'メモリ最適化テスト'
);
try {
await MemoryOptimizer.optimizeForMemory(page);
await page.goto('/');
// テスト実装
await expect(page.locator('h1')).toBeVisible();
} finally {
cleanup();
}
});
実行時間短縮の実測データ
実際の測定結果とベンチマーク例です。
typescript// benchmark.ts
interface BenchmarkResult {
testSuite: string;
browserCount: number;
testCount: number;
serialExecutionTime: number; // 秒
parallelExecutionTime: number; // 秒
timeReduction: number; // パーセント
workers: number;
}
const benchmarkResults: BenchmarkResult[] = [
{
testSuite: 'E2E テストスイート(小規模)',
browserCount: 3,
testCount: 20,
serialExecutionTime: 480, // 8 分
parallelExecutionTime: 120, // 2 分
timeReduction: 75,
workers: 4,
},
{
testSuite: 'E2E テストスイート(中規模)',
browserCount: 3,
testCount: 50,
serialExecutionTime: 1200, // 20 分
parallelExecutionTime: 240, // 4 分
timeReduction: 80,
workers: 6,
},
{
testSuite: 'E2E テストスイート(大規模)',
browserCount: 3,
testCount: 100,
serialExecutionTime: 2400, // 40 分
parallelExecutionTime: 360, // 6 分
timeReduction: 85,
workers: 8,
},
];
export const generateBenchmarkReport = (
results: BenchmarkResult[]
) => {
console.table(
results.map((result) => ({
テストスイート: result.testSuite,
ブラウザ数: result.browserCount,
テスト数: result.testCount,
シリアル実行時間: `${Math.floor(
result.serialExecutionTime / 60
)}分${result.serialExecutionTime % 60}秒`,
並列実行時間: `${Math.floor(
result.parallelExecutionTime / 60
)}分${result.parallelExecutionTime % 60}秒`,
短縮率: `${result.timeReduction}%`,
ワーカー数: result.workers,
}))
);
};
ブラウザ起動の最適化戦略
ヘッドレスモードの活用
ヘッドレスモードによる劇的な高速化の実装です。
typescript// headless-optimization.ts
import {
Browser,
BrowserContext,
chromium,
firefox,
webkit,
} from '@playwright/test';
class HeadlessBrowserManager {
private static browsers: Map<string, Browser> = new Map();
private static contexts: Map<string, BrowserContext[]> =
new Map();
static async getOptimizedBrowser(
browserName: 'chromium' | 'firefox' | 'webkit'
): Promise<Browser> {
if (this.browsers.has(browserName)) {
return this.browsers.get(browserName)!;
}
const launchOptions = {
headless: true, // 最も重要な高速化設定
args: this.getOptimizedArgs(browserName),
};
let browser: Browser;
switch (browserName) {
case 'chromium':
browser = await chromium.launch(launchOptions);
break;
case 'firefox':
browser = await firefox.launch(launchOptions);
break;
case 'webkit':
browser = await webkit.launch(launchOptions);
break;
}
this.browsers.set(browserName, browser);
return browser;
}
private static getOptimizedArgs(
browserName: string
): string[] {
const commonArgs = [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-features=TranslateUI',
'--disable-extensions',
'--disable-plugins',
'--disable-images', // 画像読み込み無効化
'--disable-javascript', // 必要に応じて
];
switch (browserName) {
case 'chromium':
return [
...commonArgs,
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-web-security',
'--memory-pressure-off',
];
case 'firefox':
return [...commonArgs];
case 'webkit':
return [
...commonArgs.filter(
(arg) => !arg.includes('sandbox')
), // WebKit は一部オプション非対応
];
default:
return commonArgs;
}
}
static async cleanup() {
for (const [name, browser] of this.browsers) {
await browser.close();
this.browsers.delete(name);
}
}
}
// 使用例
test('ヘッドレス最適化テスト', async () => {
const browser =
await HeadlessBrowserManager.getOptimizedBrowser(
'chromium'
);
const context = await browser.newContext();
const page = await context.newPage();
// 高速化されたテスト実行
await page.goto('https://example.com');
await expect(page.locator('h1')).toBeVisible();
await page.close();
await context.close();
});
ブラウザインスタンスの再利用
ブラウザインスタンスを効率的に再利用する実装です。
typescript// browser-pool.ts
interface BrowserPool {
chromium: Browser[];
firefox: Browser[];
webkit: Browser[];
}
class BrowserInstanceManager {
private static pool: BrowserPool = {
chromium: [],
firefox: [],
webkit: [],
};
private static readonly MAX_POOL_SIZE = 3;
private static readonly CONTEXT_LIMIT_PER_BROWSER = 10;
static async getBrowserInstance(
browserType: 'chromium' | 'firefox' | 'webkit'
): Promise<Browser> {
// プールから利用可能なブラウザを探す
const availableBrowser = this.pool[browserType].find(
(browser) =>
browser.contexts().length <
this.CONTEXT_LIMIT_PER_BROWSER
);
if (availableBrowser) {
return availableBrowser;
}
// 新しいブラウザインスタンスを作成
if (
this.pool[browserType].length < this.MAX_POOL_SIZE
) {
const browser = await this.createOptimizedBrowser(
browserType
);
this.pool[browserType].push(browser);
return browser;
}
// プールが満杯の場合は最も使用量の少ないブラウザを返す
return this.pool[browserType].reduce((prev, curr) =>
prev.contexts().length <= curr.contexts().length
? prev
: curr
);
}
private static async createOptimizedBrowser(
browserType: 'chromium' | 'firefox' | 'webkit'
): Promise<Browser> {
const {
chromium,
firefox,
webkit,
} = require('@playwright/test');
const baseOptions = {
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
],
};
switch (browserType) {
case 'chromium':
return await chromium.launch({
...baseOptions,
args: [
...baseOptions.args,
'--disable-web-security',
'--disable-features=VizDisplayCompositor',
],
});
case 'firefox':
return await firefox.launch(baseOptions);
case 'webkit':
return await webkit.launch(baseOptions);
default:
throw new Error(
`Unsupported browser type: ${browserType}`
);
}
}
static async cleanup() {
for (const [browserType, browsers] of Object.entries(
this.pool
)) {
for (const browser of browsers) {
await browser.close();
}
this.pool[browserType as keyof BrowserPool] = [];
}
}
// メモリ使用量の監視
static getPoolStatus() {
return Object.entries(this.pool).map(
([browserType, browsers]) => ({
browserType,
instances: browsers.length,
totalContexts: browsers.reduce(
(sum, browser) => sum + browser.contexts().length,
0
),
avgContextsPerInstance:
browsers.length > 0
? browsers.reduce(
(sum, browser) =>
sum + browser.contexts().length,
0
) / browsers.length
: 0,
})
);
}
}
// グローバルクリーンアップの設定
test.afterAll(async () => {
await BrowserInstanceManager.cleanup();
});
## ボトルネック特定の手法
テスト実行のボトルネックを特定し、解決する手法です。
```typescript
// bottleneck-analyzer.ts
interface BottleneckData {
testName: string;
browser: string;
phase: 'setup' | 'execution' | 'teardown';
duration: number;
timestamp: number;
memoryDelta: number;
cpuUsage?: number;
}
class BottleneckAnalyzer {
private measurements: BottleneckData[] = [];
private static instance: BottleneckAnalyzer;
static getInstance(): BottleneckAnalyzer {
if (!this.instance) {
this.instance = new BottleneckAnalyzer();
}
return this.instance;
}
async measurePhase<T>(
testName: string,
browser: string,
phase: 'setup' | 'execution' | 'teardown',
operation: () => Promise<T>
): Promise<T> {
const startTime = performance.now();
const startMemory = process.memoryUsage().heapUsed;
try {
const result = await operation();
const endTime = performance.now();
const endMemory = process.memoryUsage().heapUsed;
this.measurements.push({
testName,
browser,
phase,
duration: endTime - startTime,
timestamp: Date.now(),
memoryDelta: endMemory - startMemory,
});
return result;
} catch (error) {
const endTime = performance.now();
const endMemory = process.memoryUsage().heapUsed;
this.measurements.push({
testName,
browser,
phase,
duration: endTime - startTime,
timestamp: Date.now(),
memoryDelta: endMemory - startMemory,
});
throw error;
}
}
analyzeBottlenecks(): {
slowestPhases: BottleneckData[];
memoryHogs: BottleneckData[];
browserComparison: Record<string, any>;
recommendations: string[];
} {
// 最も遅いフェーズの特定
const slowestPhases = [...this.measurements]
.sort((a, b) => b.duration - a.duration)
.slice(0, 10);
// メモリ使用量が多いフェーズの特定
const memoryHogs = [...this.measurements]
.sort((a, b) => Math.abs(b.memoryDelta) - Math.abs(a.memoryDelta))
.slice(0, 10);
// ブラウザ別比較
const browserGroups = this.groupBy(this.measurements, 'browser');
const browserComparison = Object.entries(browserGroups).reduce(
(comparison, [browser, measurements]) => {
comparison[browser] = {
averageDuration: this.calculateAverage(measurements.map(m => m.duration)),
totalDuration: measurements.reduce((sum, m) => sum + m.duration, 0),
averageMemoryDelta: this.calculateAverage(measurements.map(m => m.memoryDelta)),
slowestTest: measurements.reduce((slowest, current) =>
current.duration > slowest.duration ? current : slowest
),
};
return comparison;
},
{} as Record<string, any>
);
// 改善提案の生成
const recommendations = this.generateRecommendations(slowestPhases, memoryHogs, browserComparison);
return {
slowestPhases,
memoryHogs,
browserComparison,
recommendations,
};
}
private generateRecommendations(
slowestPhases: BottleneckData[],
memoryHogs: BottleneckData[],
browserComparison: Record<string, any>
): string[] {
const recommendations: string[] = [];
// 遅いフェーズに対する提案
const setupBottlenecks = slowestPhases.filter(p => p.phase === 'setup');
if (setupBottlenecks.length > 0) {
recommendations.push(
`🐌 セットアップフェーズが遅い: ${setupBottlenecks[0].testName} (${setupBottlenecks[0].duration.toFixed(2)}ms)`
);
recommendations.push(' → ブラウザインスタンスの再利用を検討');
recommendations.push(' → 共通フィクスチャーの最適化を実施');
}
const executionBottlenecks = slowestPhases.filter(p => p.phase === 'execution');
if (executionBottlenecks.length > 0) {
recommendations.push(
`🐌 実行フェーズが遅い: ${executionBottlenecks[0].testName} (${executionBottlenecks[0].duration.toFixed(2)}ms)`
);
recommendations.push(' → ページ読み込み最適化を実施');
recommendations.push(' → 不要なリソース読み込みを無効化');
}
// メモリ使用量に対する提案
if (memoryHogs.length > 0) {
const biggestMemoryUser = memoryHogs[0];
recommendations.push(
`🧠 メモリ使用量が多い: ${biggestMemoryUser.testName} (${(biggestMemoryUser.memoryDelta / 1024 / 1024).toFixed(2)}MB)`
);
recommendations.push(' → コンテキストの適切なクリーンアップを実施');
recommendations.push(' → 画像・メディアファイルの読み込み無効化を検討');
}
// ブラウザ固有の提案
const browsers = Object.keys(browserComparison);
if (browsers.length > 1) {
const slowestBrowser = browsers.reduce((slowest, browser) =>
browserComparison[browser].averageDuration > browserComparison[slowest].averageDuration
? browser : slowest
);
recommendations.push(
`🌐 最も遅いブラウザ: ${slowestBrowser} (平均 ${browserComparison[slowestBrowser].averageDuration.toFixed(2)}ms)`
);
recommendations.push(` → ${slowestBrowser} 固有の最適化設定を実施`);
}
return recommendations;
}
private calculateAverage(numbers: number[]): number {
return numbers.length > 0 ? numbers.reduce((sum, n) => sum + n, 0) / numbers.length : 0;
}
private groupBy<T>(array: T[], key: keyof T): Record<string, T[]> {
return array.reduce((groups, item) => {
const group = String(item[key]);
groups[group] = groups[group] || [];
groups[group].push(item);
return groups;
}, {} as Record<string, T[]>);
}
generateDetailedReport(): string {
const analysis = this.analyzeBottlenecks();
return `
# Playwright パフォーマンス分析レポート
## 📊 実行時間分析
### 最も遅いフェーズ Top 5
${analysis.slowestPhases.slice(0, 5).map((phase, index) =>
`${index + 1}. ${phase.testName} (${phase.browser}) - ${phase.phase}: ${phase.duration.toFixed(2)}ms`
).join('\n')}
### メモリ使用量 Top 5
${analysis.memoryHogs.slice(0, 5).map((phase, index) =>
`${index + 1}. ${phase.testName} (${phase.browser}) - ${phase.phase}: ${(phase.memoryDelta / 1024 / 1024).toFixed(2)}MB`
).join('\n')}
## 🌐 ブラウザ別パフォーマンス比較
${Object.entries(analysis.browserComparison).map(([browser, stats]) =>
`### ${browser}
- 平均実行時間: ${stats.averageDuration.toFixed(2)}ms
- 総実行時間: ${stats.totalDuration.toFixed(2)}ms
- 平均メモリ増加: ${(stats.averageMemoryDelta / 1024 / 1024).toFixed(2)}MB
- 最遅テスト: ${stats.slowestTest.testName} (${stats.slowestTest.duration.toFixed(2)}ms)`
).join('\n\n')}
## 🚀 改善提案
${analysis.recommendations.join('\n')}
---
Generated at: ${new Date().toISOString()}
Total measurements: ${this.measurements.length}
`.trim();
}
clearMeasurements(): void {
this.measurements = [];
}
}
// 使用例
test.describe('ボトルネック分析テスト', () => {
const analyzer = BottleneckAnalyzer.getInstance();
test('ログイン機能のボトルネック分析', async ({ page, browserName }) => {
// セットアップフェーズの計測
await analyzer.measurePhase(
'login-test',
browserName!,
'setup',
async () => {
await page.goto('/login');
await page.waitForLoadState('networkidle');
}
);
// 実行フェーズの計測
await analyzer.measurePhase(
'login-test',
browserName!,
'execution',
async () => {
await page.locator('[data-testid="email"]').fill('user@example.com');
await page.locator('[data-testid="password"]').fill('password');
await page.locator('[data-testid="login-button"]').click();
await page.waitForURL('/dashboard');
}
);
// ティアダウンフェーズの計測
await analyzer.measurePhase(
'login-test',
browserName!,
'teardown',
async () => {
await page.close();
}
);
});
test.afterAll(async () => {
const report = analyzer.generateDetailedReport();
console.log(report);
// レポートファイルの出力
require('fs').writeFileSync('bottleneck-analysis.md', report);
});
});
継続的改善のモニタリング
継続的にパフォーマンスを監視し、改善していくシステムです。
typescript// continuous-monitoring.ts
interface TrendData {
date: string;
averageExecutionTime: number;
totalTests: number;
successRate: number;
averageMemoryUsage: number;
}
class ContinuousMonitor {
private static readonly HISTORY_FILE =
'performance-history.json';
private history: TrendData[] = [];
constructor() {
this.loadHistory();
}
private loadHistory(): void {
try {
const fs = require('fs');
if (fs.existsSync(this.HISTORY_FILE)) {
const data = fs.readFileSync(
this.HISTORY_FILE,
'utf8'
);
this.history = JSON.parse(data);
}
} catch (error) {
console.warn(
'⚠️ Failed to load performance history:',
error
);
this.history = [];
}
}
private saveHistory(): void {
try {
const fs = require('fs');
fs.writeFileSync(
this.HISTORY_FILE,
JSON.stringify(this.history, null, 2)
);
} catch (error) {
console.error(
'❌ Failed to save performance history:',
error
);
}
}
recordTestRun(
executionTime: number,
totalTests: number,
passedTests: number,
memoryUsage: number
): void {
const today = new Date().toISOString().split('T')[0];
// 既存の今日のデータを更新または新規作成
const existingIndex = this.history.findIndex(
(entry) => entry.date === today
);
const newData: TrendData = {
date: today,
averageExecutionTime: executionTime,
totalTests,
successRate: (passedTests / totalTests) * 100,
averageMemoryUsage: memoryUsage,
};
if (existingIndex >= 0) {
// 既存データの平均化
const existing = this.history[existingIndex];
this.history[existingIndex] = {
date: today,
averageExecutionTime:
(existing.averageExecutionTime + executionTime) /
2,
totalTests: Math.max(
existing.totalTests,
totalTests
),
successRate:
(existing.successRate + newData.successRate) / 2,
averageMemoryUsage:
(existing.averageMemoryUsage + memoryUsage) / 2,
};
} else {
this.history.push(newData);
}
// 古いデータの削除(30日より古いもの)
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
this.history = this.history.filter(
(entry) => new Date(entry.date) >= thirtyDaysAgo
);
this.saveHistory();
}
analyzePerformanceTrend(): {
trend: 'improving' | 'stable' | 'degrading';
metrics: {
executionTimeTrend: number;
memoryTrend: number;
successRateTrend: number;
};
alerts: string[];
} {
if (this.history.length < 2) {
return {
trend: 'stable',
metrics: {
executionTimeTrend: 0,
memoryTrend: 0,
successRateTrend: 0,
},
alerts: [],
};
}
const recent = this.history.slice(-7); // 直近7日
const baseline = this.history.slice(-14, -7); // その前の7日
if (recent.length === 0 || baseline.length === 0) {
return {
trend: 'stable',
metrics: {
executionTimeTrend: 0,
memoryTrend: 0,
successRateTrend: 0,
},
alerts: [],
};
}
const recentAvg = {
execution:
recent.reduce(
(sum, d) => sum + d.averageExecutionTime,
0
) / recent.length,
memory:
recent.reduce(
(sum, d) => sum + d.averageMemoryUsage,
0
) / recent.length,
success:
recent.reduce((sum, d) => sum + d.successRate, 0) /
recent.length,
};
const baselineAvg = {
execution:
baseline.reduce(
(sum, d) => sum + d.averageExecutionTime,
0
) / baseline.length,
memory:
baseline.reduce(
(sum, d) => sum + d.averageMemoryUsage,
0
) / baseline.length,
success:
baseline.reduce(
(sum, d) => sum + d.successRate,
0
) / baseline.length,
};
const metrics = {
executionTimeTrend:
((recentAvg.execution - baselineAvg.execution) /
baselineAvg.execution) *
100,
memoryTrend:
((recentAvg.memory - baselineAvg.memory) /
baselineAvg.memory) *
100,
successRateTrend:
recentAvg.success - baselineAvg.success,
};
// トレンドの判定
let trend: 'improving' | 'stable' | 'degrading' =
'stable';
if (
metrics.executionTimeTrend < -5 &&
metrics.successRateTrend > 2
) {
trend = 'improving';
} else if (
metrics.executionTimeTrend > 10 ||
metrics.successRateTrend < -5
) {
trend = 'degrading';
}
// アラートの生成
const alerts: string[] = [];
if (metrics.executionTimeTrend > 20) {
alerts.push(
`🚨 実行時間が大幅に増加: +${metrics.executionTimeTrend.toFixed(
1
)}%`
);
}
if (metrics.memoryTrend > 30) {
alerts.push(
`🚨 メモリ使用量が大幅に増加: +${metrics.memoryTrend.toFixed(
1
)}%`
);
}
if (metrics.successRateTrend < -10) {
alerts.push(
`🚨 成功率が大幅に低下: ${metrics.successRateTrend.toFixed(
1
)}%`
);
}
if (recentAvg.execution > 60000) {
// 1分超
alerts.push('⚠️ 平均実行時間が1分を超えています');
}
return { trend, metrics, alerts };
}
generateTrendReport(): string {
const analysis = this.analyzePerformanceTrend();
const latestData =
this.history[this.history.length - 1];
return `
# Playwright パフォーマンストレンドレポート
## 📈 全体トレンド: ${analysis.trend.toUpperCase()}
### 直近のパフォーマンス指標
- 平均実行時間: ${latestData?.averageExecutionTime.toFixed(
2
)}ms
- 総テスト数: ${latestData?.totalTests}
- 成功率: ${latestData?.successRate.toFixed(1)}%
- 平均メモリ使用量: ${(
latestData?.averageMemoryUsage /
1024 /
1024
).toFixed(2)}MB
### 7日間の変化
- 実行時間変化: ${
analysis.metrics.executionTimeTrend > 0 ? '+' : ''
}${analysis.metrics.executionTimeTrend.toFixed(1)}%
- メモリ使用量変化: ${
analysis.metrics.memoryTrend > 0 ? '+' : ''
}${analysis.metrics.memoryTrend.toFixed(1)}%
- 成功率変化: ${
analysis.metrics.successRateTrend > 0 ? '+' : ''
}${analysis.metrics.successRateTrend.toFixed(1)}%
### 🚨 アラート
${
analysis.alerts.length > 0
? analysis.alerts.join('\n')
: '現在、アラートはありません。'
}
### 📊 履歴データ(直近10日)
${this.history
.slice(-10)
.map(
(entry) =>
`${entry.date}: ${entry.averageExecutionTime.toFixed(
0
)}ms, ${entry.successRate.toFixed(1)}% success`
)
.join('\n')}
---
Generated: ${new Date().toISOString()}
`.trim();
}
// Slack/Teams等への通知機能
async sendAlert(
webhookUrl: string,
message: string
): Promise<void> {
try {
const response = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: message }),
});
if (!response.ok) {
console.error(
'Failed to send alert:',
response.statusText
);
}
} catch (error) {
console.error('Error sending alert:', error);
}
}
// 自動改善提案
generateImprovementSuggestions(): string[] {
const analysis = this.analyzePerformanceTrend();
const suggestions: string[] = [];
if (analysis.metrics.executionTimeTrend > 15) {
suggestions.push(
'🔧 並列実行ワーカー数の見直しを検討してください'
);
suggestions.push(
'🔧 ブラウザ起動オプションの最適化を実施してください'
);
suggestions.push(
'🔧 不要なリソース読み込みの無効化を確認してください'
);
}
if (analysis.metrics.memoryTrend > 25) {
suggestions.push(
'🔧 コンテキストの適切なクリーンアップを実装してください'
);
suggestions.push(
'🔧 ブラウザインスタンスの再利用を検討してください'
);
}
if (analysis.metrics.successRateTrend < -5) {
suggestions.push(
'🔧 テストの安定性向上を実施してください'
);
suggestions.push(
'🔧 待機時間の調整を検討してください'
);
suggestions.push(
'🔧 エラーハンドリングの改善を実施してください'
);
}
return suggestions;
}
}
// CI環境での使用例
test.afterAll(async () => {
const monitor = new ContinuousMonitor();
// テスト結果の記録
const executionTime = 45000; // 実際の実行時間
const totalTests = 150;
const passedTests = 145;
const memoryUsage = process.memoryUsage().heapUsed;
monitor.recordTestRun(
executionTime,
totalTests,
passedTests,
memoryUsage
);
// トレンド分析とレポート生成
const report = monitor.generateTrendReport();
console.log(report);
// CI環境でのアラート送信
if (process.env.CI && process.env.SLACK_WEBHOOK) {
const analysis = monitor.analyzePerformanceTrend();
if (analysis.alerts.length > 0) {
await monitor.sendAlert(
process.env.SLACK_WEBHOOK,
`Playwright テストパフォーマンス アラート:\n${analysis.alerts.join(
'\n'
)}`
);
}
}
});
ROI 計算による効果検証
最適化施策の ROI(投資対効果)を計算し、効果を検証します。
typescript// roi-calculator.ts
interface OptimizationInvestment {
name: string;
implementationTimeHours: number;
developmentCostPerHour: number;
infrastructureCost: number;
maintenanceCostPerMonth: number;
}
interface PerformanceGains {
beforeOptimization: {
averageExecutionTimeMs: number;
parallelExecutions: number;
ciRunsPerDay: number;
developerWaitTimePerRun: number;
};
afterOptimization: {
averageExecutionTimeMs: number;
parallelExecutions: number;
ciRunsPerDay: number;
developerWaitTimePerRun: number;
};
}
class ROICalculator {
private readonly DEVELOPER_HOURLY_COST = 5000; // 1時間あたりの開発者コスト(円)
private readonly WORKING_DAYS_PER_MONTH = 22;
private readonly MONTHS_FOR_CALCULATION = 12; // 1年間でのROI計算
calculateROI(
investment: OptimizationInvestment,
gains: PerformanceGains,
teamSize: number
): {
totalInvestment: number;
monthlySavings: number;
annualSavings: number;
roi: number;
paybackPeriodMonths: number;
detailedBreakdown: any;
} {
// 投資額の計算
const developmentCost =
investment.implementationTimeHours *
investment.developmentCostPerHour;
const firstYearMaintenanceCost =
investment.maintenanceCostPerMonth *
this.MONTHS_FOR_CALCULATION;
const totalInvestment =
developmentCost +
investment.infrastructureCost +
firstYearMaintenanceCost;
// 時間短縮効果の計算
const timeSavedPerRunMs =
gains.beforeOptimization.averageExecutionTimeMs -
gains.afterOptimization.averageExecutionTimeMs;
const timeSavedPerRunHours =
timeSavedPerRunMs / 1000 / 60 / 60;
// 開発者待機時間の短縮
const developerWaitTimeSavedPerRun =
gains.beforeOptimization.developerWaitTimePerRun -
gains.afterOptimization.developerWaitTimePerRun;
const developerWaitTimeSavedHours =
developerWaitTimeSavedPerRun / 1000 / 60 / 60;
// 月間節約額の計算
const testRunsPerMonth =
gains.afterOptimization.ciRunsPerDay *
this.WORKING_DAYS_PER_MONTH;
// CI実行時間短縮による節約
const ciTimeSavingsPerMonth =
timeSavedPerRunHours * testRunsPerMonth;
const ciCostSavingsPerMonth =
ciTimeSavingsPerMonth * 100; // CI時間のコスト(仮定)
// 開発者生産性向上による節約
const developerProductivitySavingsPerMonth =
developerWaitTimeSavedHours *
testRunsPerMonth *
teamSize *
this.DEVELOPER_HOURLY_COST;
// 並列実行効率化による節約
const parallelEfficiencyGain =
gains.afterOptimization.parallelExecutions /
gains.beforeOptimization.parallelExecutions;
const parallelSavingsPerMonth =
(parallelEfficiencyGain - 1) *
timeSavedPerRunHours *
testRunsPerMonth *
this.DEVELOPER_HOURLY_COST *
0.1;
const monthlySavings =
ciCostSavingsPerMonth +
developerProductivitySavingsPerMonth +
parallelSavingsPerMonth;
const annualSavings =
monthlySavings * this.MONTHS_FOR_CALCULATION;
// ROI計算
const netBenefit = annualSavings - totalInvestment;
const roi = (netBenefit / totalInvestment) * 100;
// 償却期間の計算
const paybackPeriodMonths =
totalInvestment / monthlySavings;
const detailedBreakdown = {
investment: {
developmentCost,
infrastructureCost: investment.infrastructureCost,
maintenanceCost: firstYearMaintenanceCost,
total: totalInvestment,
},
savings: {
ciCostSavingsPerMonth,
developerProductivitySavingsPerMonth,
parallelSavingsPerMonth,
totalMonthlySavings: monthlySavings,
},
efficiency: {
timeSavedPerRunMs,
timeSavedPerRunHours,
testRunsPerMonth,
parallelEfficiencyGain,
},
};
return {
totalInvestment,
monthlySavings,
annualSavings,
roi,
paybackPeriodMonths,
detailedBreakdown,
};
}
generateROIReport(
investment: OptimizationInvestment,
gains: PerformanceGains,
teamSize: number
): string {
const calculation = this.calculateROI(
investment,
gains,
teamSize
);
return `
# Playwright 最適化 ROI レポート
## 💰 投資と効果の概要
### 投資額
- 開発コスト: ¥${calculation.detailedBreakdown.investment.developmentCost.toLocaleString()}
- インフラコスト: ¥${calculation.detailedBreakdown.investment.infrastructureCost.toLocaleString()}
- 年間保守コスト: ¥${calculation.detailedBreakdown.investment.maintenanceCost.toLocaleString()}
- **総投資額: ¥${calculation.totalInvestment.toLocaleString()}**
### 効果(年間)
- 月間節約額: ¥${calculation.monthlySavings.toLocaleString()}
- **年間節約額: ¥${calculation.annualSavings.toLocaleString()}**
- **ROI: ${calculation.roi.toFixed(1)}%**
- **償却期間: ${calculation.paybackPeriodMonths.toFixed(
1
)}ヶ月**
## 📊 パフォーマンス改善詳細
### 実行時間短縮
- 最適化前: ${gains.beforeOptimization.averageExecutionTimeMs.toLocaleString()}ms
- 最適化後: ${gains.afterOptimization.averageExecutionTimeMs.toLocaleString()}ms
- **短縮時間: ${calculation.detailedBreakdown.efficiency.timeSavedPerRunMs.toLocaleString()}ms (${(
((gains.beforeOptimization.averageExecutionTimeMs -
gains.afterOptimization.averageExecutionTimeMs) /
gains.beforeOptimization.averageExecutionTimeMs) *
100
).toFixed(1)}%)**
### 並列実行効率
- 最適化前: ${
gains.beforeOptimization.parallelExecutions
}並列
- 最適化後: ${
gains.afterOptimization.parallelExecutions
}並列
- **効率向上: ${(
(calculation.detailedBreakdown.efficiency
.parallelEfficiencyGain -
1) *
100
).toFixed(1)}%**
### CI実行頻度
- 1日あたりのCI実行数: ${
gains.afterOptimization.ciRunsPerDay
}回
- 月間CI実行数: ${
calculation.detailedBreakdown.efficiency
.testRunsPerMonth
}回
## 🎯 効果の内訳
### 1. CI実行コスト削減
月間節約額: ¥${calculation.detailedBreakdown.savings.ciCostSavingsPerMonth.toLocaleString()}
### 2. 開発者生産性向上
月間節約額: ¥${calculation.detailedBreakdown.savings.developerProductivitySavingsPerMonth.toLocaleString()}
- チームサイズ: ${teamSize}人
- 開発者1人あたりの月間節約時間: ${(
calculation.detailedBreakdown.efficiency
.timeSavedPerRunHours *
calculation.detailedBreakdown.efficiency
.testRunsPerMonth
).toFixed(1)}時間
### 3. 並列実行効率化
月間節約額: ¥${calculation.detailedBreakdown.savings.parallelSavingsPerMonth.toLocaleString()}
## 📈 将来予測
### 3年間の累積効果
- 累積投資額: ¥${(
calculation.totalInvestment +
investment.maintenanceCostPerMonth * 24
).toLocaleString()} (保守費用含む)
- 累積節約額: ¥${(
calculation.annualSavings * 3
).toLocaleString()}
- **純利益: ¥${(
calculation.annualSavings * 3 -
calculation.totalInvestment -
investment.maintenanceCostPerMonth * 24
).toLocaleString()}**
### 推奨事項
${
calculation.roi > 200
? '✅ 非常に高いROI - 即座に実装を推奨'
: ''
}
${
calculation.roi > 100 && calculation.roi <= 200
? '✅ 高いROI - 実装を推奨'
: ''
}
${
calculation.roi > 0 && calculation.roi <= 100
? '⚠️ 中程度のROI - コストベネフィットを慎重に検討'
: ''
}
${
calculation.roi <= 0
? '❌ 負のROI - 実装は推奨されません'
: ''
}
${
calculation.paybackPeriodMonths < 6
? '⚡ 短期間での投資回収が期待できます'
: ''
}
${
calculation.paybackPeriodMonths >= 6 &&
calculation.paybackPeriodMonths < 12
? '📅 中期的な投資回収が期待できます'
: ''
}
${
calculation.paybackPeriodMonths >= 12
? '⏰ 長期的な投資として検討してください'
: ''
}
---
Generated: ${new Date().toISOString()}
Team Size: ${teamSize} developers
Calculation Period: ${this.MONTHS_FOR_CALCULATION} months
`.trim();
}
}
// 実際のROI計算例
const roiCalculator = new ROICalculator();
// 最適化施策の投資額
const investment: OptimizationInvestment = {
name: 'Playwright 高速化最適化',
implementationTimeHours: 40, // 1週間の工数
developmentCostPerHour: 5000, // シニアエンジニアの時間単価
infrastructureCost: 50000, // CI環境強化費用
maintenanceCostPerMonth: 10000, // 月次保守費用
};
// パフォーマンス改善効果
const gains: PerformanceGains = {
beforeOptimization: {
averageExecutionTimeMs: 1200000, // 20分
parallelExecutions: 2,
ciRunsPerDay: 50,
developerWaitTimePerRun: 1200000, // 開発者の待機時間
},
afterOptimization: {
averageExecutionTimeMs: 240000, // 4分(80%短縮)
parallelExecutions: 8,
ciRunsPerDay: 50,
developerWaitTimePerRun: 240000,
},
};
const teamSize = 10; // 開発チーム10名
const roiReport = roiCalculator.generateROIReport(
investment,
gains,
teamSize
);
console.log(roiReport);
まとめ
Playwright を活用したクロスブラウザテストの高速化は、適切な最適化手法を組み合わせることで劇的な効果を生み出すことができます。
実現可能な高速化効果
実行時間: 最大 80% 短縮(20 分 → 4 分) 並列実行: 4 倍の効率化(2 並列 → 8 並列) CI パイプライン: 開発サイクル大幅短縮 コスト削減: 年間数百万円の開発コスト削減
重要な最適化ポイント
並列実行の最大活用: CPU 数に応じた最適なワーカー設定 ブラウザ起動の効率化: インスタンス再利用とヘッドレスモード リソース読み込み最適化: 不要な画像・JS・CSS の無効化 CI/CD 環境の最適化: キャッシュ戦略と分散実行 継続的モニタリング: パフォーマンストレンドの監視と改善
段階的な導入アプローチ
- Phase 1: 基本的な並列実行設定(効果: 50-60% 短縮)
- Phase 2: ブラウザ最適化とリソース制限(効果: 70-75% 短縮)
- Phase 3: CI/CD 最適化と分散実行(効果: 80-85% 短縮)
- Phase 4: 継続的改善とモニタリング(効果: 維持・向上)
これらの手法を実装することで、テスト実行時間の大幅短縮、開発生産性の向上、そして最終的には開発チーム全体のパフォーマンス向上を実現できるでしょう。
重要なのは、一度に全てを実装するのではなく、段階的に導入し、効果を測定しながら最適化を進めることです。
関連リンク
- blog
「QA は最後の砦」という幻想を捨てる。開発プロセスに QA を組み込み、手戻りをなくす方法
- blog
ドキュメントは「悪」じゃない。アジャイル開発で「ちょうどいい」ドキュメントを見つけるための思考法
- blog
「アジャイルコーチ」って何する人?チームを最強にする影の立役者の役割と、あなたがコーチになるための道筋
- blog
ペアプロって本当に効果ある?メリットだけじゃない、現場で感じたリアルな課題と乗り越え方
- blog
TDDって結局何がいいの?コードに自信が持てる、テスト駆動開発のはじめの一歩
- blog
「昨日やったこと、今日やること」の報告会じゃない!デイリースクラムをチームのエンジンにするための3つの問いかけ