Day 9: Parallelism and Performance
What You'll Learn Today
- Playwright's parallelism model (Workers)
- Configuring workers and fullyParallel mode
- test.describe.serial() for sequential tests
- Sharding for CI environments
- Retries and flaky test strategies
- Timeout types and configuration
- Performance optimization techniques
Playwright's Parallelism Model
Playwright runs test files in parallel by default. Each test file is executed in an isolated Worker process, preventing interference between tests.
flowchart TB
subgraph Runner["Test Runner"]
R["playwright test"]
end
subgraph Workers["Worker Processes"]
W1["Worker 1<br/>login.spec.ts"]
W2["Worker 2<br/>cart.spec.ts"]
W3["Worker 3<br/>search.spec.ts"]
end
subgraph Browsers["Browser Instances"]
B1["Chromium"]
B2["Chromium"]
B3["Chromium"]
end
R --> W1 & W2 & W3
W1 --> B1
W2 --> B2
W3 --> B3
style Runner fill:#3b82f6,color:#fff
style Workers fill:#8b5cf6,color:#fff
style Browsers fill:#22c55e,color:#fff
Key points:
- One Worker handles one test file at a time
- Tests within the same file run sequentially by default
- Workers are fully isolated and share no state
Configuring Workers
In playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
// Set worker count (default: half of CPU cores)
workers: 4,
// Common pattern: limit to 1 in CI
// workers: process.env.CI ? 1 : undefined,
});
Via CLI
# Set specific worker count
npx playwright test --workers=4
# Set by CPU percentage (50%)
npx playwright test --workers=50%
# Disable parallelism (useful for debugging)
npx playwright test --workers=1
Worker Count Guidelines
| Environment | Recommended Workers | Reason |
|---|---|---|
| Local development | Half of CPU cores (default) | Leaves room for other work |
| CI (small) | 1-2 | Watch for memory limits |
| CI (large) | 4-8 | Scale based on resources |
| Debugging | 1 | Easier to read output |
fullyParallel Mode
By default, tests within the same file run sequentially. Enabling fullyParallel allows tests within a file to run in parallel as well.
import { defineConfig } from '@playwright/test';
export default defineConfig({
// Enable for the entire project
fullyParallel: true,
workers: 4,
});
You can also enable parallel mode for a specific describe block:
import { test } from '@playwright/test';
// Tests in this block run in parallel
test.describe.configure({ mode: 'parallel' });
test('test A', async ({ page }) => {
// ...
});
test('test B', async ({ page }) => {
// ...
});
When to Enable fullyParallel
- Each test is completely independent
- No data dependencies between tests
- Each test handles its own setup and teardown
test.describe.serial() for Sequential Tests
When tests depend on execution order, use serial. If a test fails, subsequent tests in the block are skipped.
import { test, expect } from '@playwright/test';
test.describe.serial('Order flow', () => {
test('Step 1: Add item to cart', async ({ page }) => {
await page.goto('/products/1');
await page.click('button:text("Add to Cart")');
await expect(page.locator('.cart-count')).toHaveText('1');
});
test('Step 2: Checkout', async ({ page }) => {
await page.goto('/cart');
await page.click('button:text("Proceed to Checkout")');
await expect(page).toHaveURL(/\/checkout/);
});
test('Step 3: Confirm order', async ({ page }) => {
await page.goto('/checkout');
await page.fill('#card-number', '4242424242424242');
await page.click('button:text("Place Order")');
await expect(page.locator('.order-confirmation')).toBeVisible();
});
});
Note: Avoid
serialwhenever possible. Keep tests independent. Useserialonly when tests genuinely need to share state across a sequence.
Sharding for CI
For large test suites, sharding distributes tests across multiple CI machines.
# Split across 3 machines
# Machine 1
npx playwright test --shard=1/3
# Machine 2
npx playwright test --shard=2/3
# Machine 3
npx playwright test --shard=3/3
GitHub Actions Sharding Example
name: Playwright Tests
on: [push]
jobs:
test:
strategy:
matrix:
shard: [1/4, 2/4, 3/4, 4/4]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test --shard=${{ matrix.shard }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: report-${{ strategy.job-index }}
path: playwright-report/
flowchart LR
subgraph CI["CI Pipeline"]
Push["git push"] --> Split["Split Tests"]
Split --> S1["Shard 1/4<br/>25 tests"]
Split --> S2["Shard 2/4<br/>25 tests"]
Split --> S3["Shard 3/4<br/>25 tests"]
Split --> S4["Shard 4/4<br/>25 tests"]
S1 & S2 & S3 & S4 --> Merge["Merge Results"]
end
style CI fill:#3b82f6,color:#fff
Configuring Retries
Retries help handle flaky tests by automatically re-running failures.
Global Configuration
import { defineConfig } from '@playwright/test';
export default defineConfig({
// Retry failed tests up to 2 times
retries: 2,
// Retry only in CI
// retries: process.env.CI ? 2 : 0,
});
Via CLI
npx playwright test --retries=2
Using Retry Information in Tests
import { test, expect } from '@playwright/test';
test('test with retry info', async ({ page }, testInfo) => {
// Current retry attempt
console.log(`Attempt: ${testInfo.retry + 1}`);
// Add extra logging on retry
if (testInfo.retry > 0) {
console.log('Retrying - enabling verbose logging');
}
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});
Flaky Test Detection
Tests that pass on retry are marked as flaky in the report.
# Run tests and view the report
npx playwright test
npx playwright show-report
Common Causes of Flaky Tests
| Cause | Solution |
|---|---|
| Timing issues | Rely on waitFor and auto-retrying assertions |
| Test data conflicts | Use unique data per test |
| Animations | Disable via page.emulateMedia or CSS |
| Network instability | Mock network requests |
| External service dependency | Use API mocking |
Timeout Types and Configuration
Playwright has several timeout layers. Understanding each one is essential for stable tests.
flowchart TB
subgraph Timeouts["Timeout Hierarchy"]
GT["Global Timeout<br/>All tests combined"]
TT["Test Timeout<br/>Per test<br/>Default: 30s"]
AT["Action Timeout<br/>click, fill, etc."]
NT["Navigation Timeout<br/>goto, reload, etc."]
ET["Expect Timeout<br/>Assertions<br/>Default: 5s"]
end
GT --> TT --> AT & NT & ET
style Timeouts fill:#f59e0b,color:#fff
Configuration
import { defineConfig } from '@playwright/test';
export default defineConfig({
// Total timeout for all tests (default: unlimited)
globalTimeout: 60 * 60 * 1000, // 1 hour
// Per-test timeout (default: 30 seconds)
timeout: 60_000, // 60 seconds
// Expect timeout
expect: {
timeout: 10_000, // 10 seconds
},
use: {
// Action timeout
actionTimeout: 15_000, // 15 seconds
// Navigation timeout
navigationTimeout: 30_000, // 30 seconds
},
});
Per-Test Timeout Overrides
import { test, expect } from '@playwright/test';
// Override timeout for a specific test
test('heavy processing test', async ({ page }) => {
test.setTimeout(120_000); // 2 minutes
await page.goto('/heavy-page');
await expect(page.locator('.loaded')).toBeVisible();
});
// Triple the default timeout
test('slow test', async ({ page }) => {
test.slow(); // Multiplies timeout by 3
await page.goto('/slow-page');
// ...
});
Performance Optimization
1. Reuse Authentication State
Save authentication state instead of logging in before every test.
// auth.setup.ts
import { test as setup, expect } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'password');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
// Save authentication state
await page.context().storageState({ path: authFile });
});
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
2. Block Unnecessary Resources
Block images, fonts, and other resources that are irrelevant to your tests.
import { test, expect } from '@playwright/test';
test('block unnecessary resources', async ({ page }) => {
// Block images, fonts, and CSS
await page.route('**/*.{png,jpg,jpeg,gif,svg,woff,woff2}', (route) => {
route.abort();
});
// Block third-party scripts
await page.route('**/analytics.js', (route) => route.abort());
await page.route('**/ads/**', (route) => route.abort());
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});
3. Global Setup for Shared Preparation
// global-setup.ts
import { chromium } from '@playwright/test';
async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
// Seed test data
await page.goto('/api/test/seed');
await browser.close();
}
export default globalSetup;
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
globalSetup: require.resolve('./global-setup'),
});
4. Measuring Test Performance
import { test, expect } from '@playwright/test';
test('measure performance', async ({ page }, testInfo) => {
const start = Date.now();
await page.goto('/');
await expect(page.locator('.content')).toBeVisible();
const duration = Date.now() - start;
console.log(`Page load took ${duration}ms`);
// Record as test annotation
testInfo.annotations.push({
type: 'performance',
description: `Load time: ${duration}ms`,
});
});
5. Optimization Checklist
| Technique | Impact | Implementation Effort |
|---|---|---|
| Reuse auth state | High | Low |
| Block unnecessary resources | Medium | Low |
| Sharding | High | Medium |
| fullyParallel | Medium | Low |
| API mocking | Medium | Medium |
| Global setup | Medium | Low |
Summary
Today we covered parallelism and performance optimization in Playwright.
| Concept | Description |
|---|---|
| Workers | Processes that run test files in parallel |
| fullyParallel | Also parallelizes tests within files |
| serial | Guarantees sequential test execution |
| Sharding | Distributes tests across CI machines |
| Retries | Automatically re-runs failed tests |
| Timeouts | Configurable at test, action, navigation, and expect levels |
| Auth state reuse | Skip login via storageState |
Next up: Day 10 covers CI/CD integration and Playwright best practices.