Answer
Scenario: Optimizing 500 Tests from 45 Minutes to Under 10 Minutes
Current State Analysis
CODE
500 tests × 5.4 seconds avg = 45 minutes
Running sequentially (1 worker, no sharding)
Login UI for every test: 500 × 5s = 41 minutes wasted
Images/analytics loaded: adds 1-2s per navigation
Optimization Plan — Targeting <10 Minutes
Step 1: Enable Parallel Workers (Free 4x Speedup)
TypeScript
// playwright.config.ts
export default defineConfig({
fullyParallel: true,
workers: 8, // 8 parallel browsers
});
CODE
500 tests / 8 workers = 62.5 tests per worker
62.5 × 5.4s = ~5.6 minutes
Step 2: storageState Auth Reuse (Eliminate Login Cost)
TypeScript
// Login ONCE per test run — not 500 times
projects: [
{ name: 'setup', testMatch: '*.setup.ts' },
{
name: 'e2e',
use: {
storageState: '.auth/user.json', // Already logged in
},
dependencies: ['setup'],
},
],
CODE
Before: 500 tests × 5s login = 41 minutes just on logins
After: 1 login = 5 seconds total
Savings: 40+ minutes
Step 3: Shard Across 8 CI Machines
YAML
# GitHub Actions matrix
strategy:
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
steps:
- run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
CODE
8 machines × 8 workers = 64 tests simultaneously
500 / 64 = ~8 tests per worker batch
8 × avg_duration ≈ target
Step 4: API Setup Instead of UI Flows
TypeScript
// BAD — UI-based test data setup: 5-8 seconds
test.beforeEach(async ({ page }) => {
await page.goto('/products/new');
await page.fill('#name', 'Test Product');
await page.click('#save'); // 8s total
});
// GOOD — API setup: 200ms
test.beforeEach(async ({ request }) => {
await request.post('/api/products', {
data: { name: `Product-${Date.now()}` },
headers: { Authorization: `Bearer ${process.env.ADMIN_TOKEN}` },
}); // 200ms total
});
Step 5: Block Unnecessary Assets
TypeScript
// playwright.config.ts
use: {
launchOptions: {
args: ['--disable-extensions', '--no-sandbox'],
},
},
// In fixture or beforeEach
await page.route('**/*.{png,jpg,jpeg,gif,svg,woff,woff2}', r => r.abort());
await page.route('**/google-analytics.com/**', r => r.abort());
await page.route('**/hotjar.com/**', r => r.abort());
Reduces each page load from 3s → 1.5s for asset-heavy pages.
Step 6: Use domcontentloaded Instead of networkidle
TypeScript
// BAD — waits for all network activity
await page.goto('/products', { waitUntil: 'networkidle' }); // 3-5s
// GOOD — wait for DOM ready, then for specific element
await page.goto('/products', { waitUntil: 'domcontentloaded' });
await page.locator('.product-list').waitFor(); // Specific wait
Results After All Optimizations
| Optimization | Time Saved |
|---|---|
| 8 workers (parallel) | 45m → 6m |
| 8-way sharding | 6m → 45s |
| storageState auth | 41m saved total |
| API setup | 2-4s per test |
| Block assets | 20-40% per navigation |
▸Combined result: 500 tests in ~6-8 minutes on CI
