---
title: "Speed up Playwright Tests in GitHub Actions | StarSling"
description: "Stop uploading Playwright traces and videos on every green run. Capture on failure only, and install just the browsers the suite uses."
url: https://starsling.dev/github-actions/optimizations/optimize-playwright
canonicalUrl: https://starsling.dev/github-actions/optimizations/optimize-playwright
---

# Speed up Playwright tests in GitHub Actions

[GitHub Actions](https://starsling.dev/github-actions) / [Optimizations](https://starsling.dev/github-actions/optimizations) / Speed up Playwright tests in GitHub Actions

- [How StarSling works](https://starsling.dev/)

Rule: ci.hygiene.playwright-artifacts. Detection mode: static. Last updated: 2026-08-21

A Playwright config capturing traces and videos unconditionally pays the recording and upload cost on every run, including the ones that pass and whose artifacts nobody ever opens; setting trace and video to failure-only keeps the same debugging evidence for red runs and removes the cost from green ones.

## Table of contents

- [Do this](#do-this)
- [Avoid this](#avoid-this)
- [How to detect it](#how-to-detect-it)
- [Tradeoffs and safety](#tradeoffs)
- [Verify it worked](#verify)
- [Related pages](#related-pages)
- [Sources](#sources)

<a id="do-this"></a>

## Do this

Playwright's trace and video recorders write to disk while the suite runs and the upload-artifact step then has to compress and transfer whatever they produced. When trace or video is set to "on", that recording and upload happens on every run regardless of outcome, so a suite that passes ninety-nine times out of a hundred still writes and uploads ninety-nine sets of traces and videos that exist only to be deleted by the artifact retention window. `on-first-retry` and `retain-on-failure` record only when a test actually needs debugging, and the CI job itself only writes an upload-artifact step conditioned on failure() so passing runs skip the step's own overhead too.

_.github/workflows/e2e.yml_

```yaml
jobs:
  e2e:
    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 chromium
      - run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: test-results
          path: test-results/
```

<a id="avoid-this"></a>

## Avoid this

_.github/workflows/e2e.yml_

```yaml
jobs:
  e2e:
    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
      - uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
      - uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: test-results/
```

<a id="how-to-detect-it"></a>

## How to detect it

1. Search Playwright config for unconditional capture: `grep -n 'trace:\|video:\|screenshot:' playwright.config.*` - a value of `'on'` (rather than `'on-first-retry'`, `'retain-on-failure'`, or `'only-on-failure'`) means every run records, pass or fail.
2. Search the workflow for the upload step: `grep -n 'actions/upload-artifact' -A5 .github/workflows/*.yml` - check whether the step has an `if: failure()` (or `if: always()` deliberately paired with a failure-only trace config) guarding it; an unconditioned upload step runs after passing jobs too.
3. Search for the browser install step: `grep -n 'playwright install' .github/workflows/*.yml` - a bare `npx playwright install --with-deps` with no browser argument installs Chromium, Firefox, and WebKit even when the suite only targets one.

<a id="tradeoffs"></a>

## Tradeoffs and safety

- on-first-retry only records once a test has already failed once and is being retried, so it needs `retries` configured above zero; with retries at zero, on-first-retry never fires and a failing test produces no trace at all.
- A flaky test that fails once and passes on retry still leaves no trace under on-first-retry, because the run's final status is green; if flake investigation matters more than storage cost, retain-on-failure records on every failure including ones later retried away.
- if: failure() skips the upload step on a run that was cancelled rather than failed; add if: failure() || cancelled() when artifacts from a cancelled run are also worth keeping.
- Restricting the install to one browser (chromium) breaks a suite that runs cross-browser projects in playwright.config.* - check the config's projects list before narrowing the install command.

<a id="verify"></a>

## Verify it worked

Hand this prompt to your coding agent (Claude Code, Cursor, and the like) to run this audit and open the fix as a reviewable PR:

```
Audit this repository's Playwright configuration and CI workflow for unconditional
artifact capture and fix what you find.

1. Read playwright.config.* and check the trace, video, and screenshot options. If any is
   set to 'on', propose changing trace to 'on-first-retry' (or 'retain-on-failure' if the
   repo does not configure retries) and video to 'retain-on-failure'.
2. Read the workflow file that runs the suite. For every actions/upload-artifact step that
   uploads a Playwright report or test-results directory, add if: failure() unless it
   already has an equivalent condition.
3. Check the playwright install step. If playwright.config.* defines projects for only one
   browser, propose narrowing npx playwright install --with-deps to name that browser
   explicitly; if multiple browser projects are configured, leave the install as is.
4. Read the Playwright trace viewer, test configuration, and CI docs linked on this page
   before editing.
5. Show the full diff and open a pull request; do not apply changes blindly. In the PR
   body, state how to verify: force a failing test and confirm a trace/video is still
   produced and uploaded, then confirm a passing run skips the upload steps.
```

Confirm the change landed:

1. Run the suite with all tests passing and confirm the workflow's upload-artifact steps show as skipped in the run summary, not executed.
2. Force one test to fail and confirm the run produces a trace (and video, if configured) and that the upload-artifact steps run and attach it.
3. Compare the job's total duration on a passing run before and after the change; a passing run should no longer include artifact compression and upload time.

<a id="related-pages"></a>

## Related pages

- [Shard tests across parallel jobs in GitHub Actions](https://starsling.dev/best-practices/github-actions/shard-tests)
- [Make vitest run mode explicit in GitHub Actions](https://starsling.dev/github-actions/optimizations/optimize-vitest)
- [Use Docker layer caching in GitHub Actions](https://starsling.dev/github-actions/optimizations/use-docker-layer-caching)

<a id="sources"></a>

## Sources

- [Playwright: Trace viewer](https://playwright.dev/docs/trace-viewer-intro)
- [Playwright: Test configuration (video, screenshot)](https://playwright.dev/docs/test-configuration)
- [Playwright: Continuous Integration](https://playwright.dev/docs/ci-intro)
- [Playwright: Browsers](https://playwright.dev/docs/browsers)
