Getting Playwright to run in CI takes about ten minutes. Getting it to run fast, with useful output, takes a bit longer. This is the setup I've settled on.

The baseline workflow

name: E2E Tests

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci
      - run: npx playwright install --with-deps chromium

      - run: npx playwright test
        env:
          BASE_URL: ${{ secrets.STAGING_URL }}

      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7

Two details matter more than they look. timeout-minutes stops a hung browser from burning half an hour of runner time. And if: ${{ !cancelled() }} on the artifact upload means you get the report especially when tests fail, which is the only time you need it.

Only install the browsers you use

npx playwright install --with-deps pulls Chromium, Firefox and WebKit. If your pull request check only runs Chromium, downloading the other two is roughly 90 seconds of waste on every single run. Name the browser explicitly and run the full cross-browser suite nightly instead.

Shard once the suite gets long

When the suite passed the ten-minute mark, sharding was the fix:

    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      # ...
      - run: npx playwright test --shard=${{ matrix.shard }}/4

Four parallel runners cut wall-clock time by roughly a factor of three. fail-fast: false is important: without it, one shard failing cancels the others and you lose the rest of the results.

Make the failure readable

A failed CI job that just says "1 test failed" sends the developer digging. Two things fix that. Add the GitHub reporter so failures annotate the pull request diff directly:

// playwright.config.ts
reporter: process.env.CI
  ? [['github'], ['html', { open: 'never' }]]
  : [['list']],

Then make sure traces are on for failures. A downloadable trace turns a five-message Slack thread into a link.

Don't test against a shared staging environment

This one cost me weeks before I learned it. If your tests run against a staging environment that other people are also deploying to, half your failures aren't yours. Either spin up the app in the workflow itself with webServer, or use a per-branch preview deployment.

// playwright.config.ts
webServer: {
  command: 'npm run start',
  url: 'http://localhost:3000',
  reuseExistingServer: !process.env.CI,
},

Where the time actually went

For my suite: browser installs were 90 seconds, npm ci without caching was 60, and unsharded execution was 18 minutes. Fixing all three took the pipeline from 22 minutes to just under six. Nobody argues about running the tests on every pull request at six minutes.