Learn Playwright in 10 DaysDay 9: Parallelism and Performance

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 serial whenever possible. Keep tests independent. Use serial only 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.