Speeding up slow Playwright suites: a step-by-step guide
A Playwright test suite that takes 45 minutes to run on CI doesn't scale. By the time developers see failures, context is lost, and the feedback loop breaks. We've seen this pattern across e-commerce checkouts, SaaS onboarding flows, and ride-hailing platforms: teams add tests, execution time balloons, and nobody waits for the results anymore.
The good news is that speed doesn't require sacrificing coverage. With focused changes to how you structure tests, manage state, and distribute work, we can cut CI time in half or more. This guide walks through four concrete strategies we've deployed in production environments.
Reuse auth: Storage state instead of logging in every test
Logging in before every test is one of the slowest anti-patterns in browser automation. A typical authentication flow—click login, fill email, enter password, wait for redirect, validate session—can take 10–15 seconds per test. Multiply that by 100 tests and you've lost 25 minutes to pure authentication overhead.
Playwright's storage state feature lets you authenticate once, save the session, and reuse it across tests. This shifts auth from per-test to once-per-suite.
// auth.setup.ts - Run once per test suite
import { test as setup } from '@playwright/test';
setup('authenticate', async ({ page }) => {
await page.goto('https://app.example.com/login');
await page.fill('input[name="email"]', 'testuser@example.com');
await page.fill('input[name="password"]', 'secure-password-123');
await page.click('button[type="submit"]');
// Wait for navigation to confirm auth succeeded
await page.waitForURL('**/dashboard');
// Save cookies and local storage to file
await page.context().storageState({ path: 'auth.json' });
});
Then in your actual tests, load that saved state:
// playwright.config.ts
export default defineConfig({
use: {
storageState: 'auth.json',
},
webServer: {
command: 'npm run dev',
port: 3000,
},
});
Now every test starts already logged in. We've seen this single change drop suite execution time by 20–30% in real applications. The trade-off is minimal: if authentication logic changes, you update one setup file, not 50 tests.
For multi-role testing (admin, user, customer), create separate storage state files and load them per test project:
// playwright.config.ts
export default defineConfig({
projects: [
{
name: 'admin',
use: { storageState: 'auth-admin.json' },
},
{
name: 'user',
use: { storageState: 'auth-user.json' },
},
],
});
API setup: Create data with API calls
UI-based data creation is another speed killer. Setting up a product catalog by clicking through forms, or creating test users by filling signup flows, adds seconds to every test initialization. When you're running hundreds of tests, these seconds compound.
Use your API directly to create test fixtures. A typical REST or GraphQL call completes in milliseconds compared to 5–10 seconds of UI interaction.
// tests/e-commerce-checkout.spec.ts
import { test, expect } from '@playwright/test';
test('complete purchase flow', async ({ page, request }) => {
// Create product via API in ~200ms
const productRes = await request.post('https://api.example.com/products', {
data: {
name: 'Test Widget',
price: 29.99,
sku: 'TEST-WIDGET-001',
stock: 50,
},
});
const product = await productRes.json();
// Create test user via API in ~150ms
const userRes = await request.post('https://api.example.com/users', {
data: {
email: `user-${Date.now()}@example.com`,
password: 'test-password',
},
});
const user = await userRes.json();
// Now test the UI flow with real data
await page.goto(`https://app.example.com/products/${product.id}`);
await page.click('button:has-text("Add to Cart")');
await page.goto('https://app.example.com/checkout');
// Assertions...
await expect(page.locator('[data-test="order-total"]')).toContainText('29.99');
});
A typical suite that creates data through the UI might spend 40% of time in setup. Moving to API calls reduces setup time to 5–10% while keeping the same coverage of actual user flows.
For sensitive operations (payments, permissions), create a separate test API endpoint or use a fixture service. For standard CRUD, direct API calls are fast and reliable.
Parallelize and shard: Use all CPU cores and machines
By default, Playwright runs tests sequentially. Modern CI runners have 4–16 cores available. Not using them is leaving speed on the table.
Playwright's built-in parallelization is straightforward. In your config:
// playwright.config.ts
export default defineConfig({
fullyParallel: true,
workers: process.env.CI ? 4 : undefined, // 4 workers on CI, auto-detect locally
webServer: {
command: 'npm run dev',
port: 3000,
reuseExistingServer: !process.env.CI,
},
});
On a typical CI machine with 4 CPU cores, fullyParallel: true will run 4 tests simultaneously. A 40-test suite that took 8 minutes sequentially might run in 2 minutes in parallel.
For larger suites across multiple machines, use sharding:
// GitHub Actions example
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
- run: npx playwright install --with-deps
- run: npx playwright test --shard=${{ matrix.shard }}/4
This distributes tests across 4 CI runners. If your full suite takes 12 minutes on one machine, sharding across 4 machines brings it down to roughly 3 minutes (accounting for setup overhead).
Key constraint: tests must be isolated. Parallelization breaks when tests depend on shared state (shared database rows, shared API keys, sequential operations). If you're using API setup and storage state, isolation is usually not a problem.
Trim scope: Move edge cases to lower-level tests
Some tests should not be in Playwright at all. Testing error handling, input validation, and business logic in the browser adds time without proportional value. These are better covered by unit tests or integration tests that run in milliseconds.
A real-world example from an e-commerce platform:
- Playwright E2E: User can complete checkout with valid card, valid shipping address, valid email.
- Unit/API test: 47 combinations of invalid inputs (wrong card format, expired card, invalid ZIP code, etc.) are rejected properly.
The E2E test takes 8 seconds. The 47 unit tests run in 200ms total. Same coverage, 40x faster.
Audit your existing tests. Look for patterns like:
- Testing every form validation error message via the browser (move to unit tests)
- Testing all permission combinations (move to API/backend tests)
- Testing currency conversions or date calculations (unit tests only)
- Testing all edge cases of a dropdown (test 2–3 scenarios in E2E, rest elsewhere)
Keep Playwright focused on user journeys: signup, login, core workflows, critical flows. A leaner suite that runs in 5 minutes is more maintainable and gets better signals than a bloated suite that nobody waits for.
Common mistakes
Mistake 1: Parallel tests with a single test database. If your tests share a database and run in parallel, you'll hit race conditions. Solution: give each worker its own database, or use transactions to roll back after each test. Many teams underestimate this and see flaky parallel tests.
Mistake 2: Reusing storage state across incompatible environments. Saving auth state in staging and replaying it in production will fail (different domains, different tokens). Keep storage state generation tied to your environment configuration.
Mistake 3: Sharding without adjusting timeout values. CI runners may have different hardware than your local machine. A test that passes locally in 5 seconds might timeout at 10 seconds on shared CI. Set conservative timeouts and monitor CI execution closely after parallelizing.
Mistake 4: Over-investing in E2E when unit tests would be faster. We've seen teams spend weeks optimizing E2E suites that would be 10x faster if half the tests were moved to unit/integration layers. Faster feedback often beats comprehensive E2E.
Key takeaways
- Reuse authentication via storage state to eliminate per-test login overhead—expect 20–30% suite speedup.
- Create test data with API calls instead of UI interaction; setup time typically drops from 40% to 5–10% of total suite time.
- Enable parallel execution and sharding; a 12-minute suite on one machine becomes 3 minutes across 4 machines.
- Move edge cases, validation, and business logic tests out of Playwright into unit or API tests; keep E2E focused on user journeys.
- Monitor CI execution closely after optimizations; different hardware can expose flakiness that local testing doesn't catch.






Comments
Loading comments…