How to speed up Playwright tests

Runners

May 2, 2025

A guide to speeding up Playwright tests.

If your Playwright experience was like mine, it started out working well.

Our tests ran quick, issues were caught early, and test times were fast. But as the number of tests grew, the longer we had to wait on jobs. My team grew and before I knew it, CI pipelines were sluggish, pull requests were stuck waiting on test results, and everyone was frustrated at how long it took to merge and ship. 

Fortunately, we’ve seen a few tricks to help frontend teams. I’ve distilled these findings down to a few concrete strategies I use to speed up tests without sacrificing coverage or reliability.

Here are the four key pillars I focus on when optimizing test suites:

  • Parallelization

  • Minimization

  • Optimization

  • Stabilization

Parallelization

Parallelizing your tests is a great first step—it spreads the workload across multiple worker processes, cutting down runtime and speeding up feedback loops.

Run Playwright Tests in Parallel

Playwright supports parallel test execution out of the box. To enable it, configure the number of workers in your playwright.config.ts:

export default defineConfig({
  use: { headless: true },
  projects: [
    { name: 'chromium', use: { browserName: 'chromium' } },
    { name: 'firefox', use: { browserName: 'firefox' } },
    { name: 'webkit', use: { browserName: 'webkit' } },
  ],
  workers: 4, // Adjust based on available CPU cores
});

More workers mean faster execution but higher CPU and memory usage.

Duration-based test sharding

This one requires more effort but pays off in the long run. You can pull test durations from the JUnit XML report, and then shard by test duration so that each worker gets roughly equal total time instead of equal file count.

Leverage Playwright Browser Contexts Instead of New Browsers

Creating a new browser instance is slow. Instead, use browser contexts to isolate sessions efficiently:

const context = await browser.newContext();
const page = await context.newPage();

Contexts allow faster test execution without incurring the cost of launching new browser instances

Minimization

Minimization is all about cutting out unnecessary overhead. Reducing extra processing, interactions, and dependencies is an easy way to speed things up and make tests more efficient. In frontend testing, every extra browser interaction adds time, so the goal here is to eliminate anything that doesn’t directly contribute to validating functionality.

Disable unneeded browser features in CI

Example:

browserType.launch({
  args: [
    '--disable-background-timer-throttling',
    '--disable-dev-shm-usage',
    '--disable-gpu',
    '--no-sandbox',
  ],
});

Disable CSS animations/transitions globally
Adding the following snippet cuts out wait-for-animation overhead everywhere. 

await context.addInitScript(() => {
  const style = document.createElement('style');
  style.textContent = `* { transition: none !important; animation: none !important; }`;
  document.head.appendChild(style);
});

Use API Calls Instead of UI Interactions When Possible

For setting up test data, use API requests instead of navigating the UI:

test('setup via API', async ({ request }) => {
  const r = await request.post('/api/createUser', { data: { name: 'John' } });
  
});

Optimization

Optimization is about making the tests themselves run as efficiently as possible. This means reducing unnecessary execution, improving selector performance, and ensuring tests only do what's necessary to validate functionality.

Run Tests with Persistent Authentication

Re-authenticating for every test slows things down. Instead, persist authentication data across tests:

// Create a persistent authentication state
const storageState = 'auth.json';
await page.context().storageState({ path: storageState });

Reuse it in tests:

const context = await browser.newContext({ 
  storageState: 'auth.json' 
});

Optimize Locators and Selectors

Avoid using XPath selectors to test individual components. XPath is generally slow because it requires full DOM traversal to find elements, scanning each node, extracting text, and matching it, which is computationally expensive.

Replace brittle and slow selectors like //div[contains(text(),'Click me')] with more efficient strategies:

Use data-testid attributes:

await page.locator('[data-testid="submit-button"]').click();

Prefer getByRole when applicable:

await page.getByRole('button', { name: 'Submit' }).click();

Reduce Unnecessary Waits

Avoid using static wait statements like await page.waitForTimeout(5000);. Instead, rely on Playwright’s built-in waiting mechanisms:

await page.waitForSelector('#element', { state: 'visible' });

Stabilization

Stabilization is about making tests reliable. Flaky tests waste time and break trust in tests, so the goal is to remove sources of flakiness and ensure consistent, repeatable results.

Enable Tracing Selectively

Tracing is helpful for debugging but slows down tests. Enable it only when needed:

use: { trace: 'on-first-retry' }

Use Test Retries to Avoid Unnecessary Reruns

Instead of rerunning an entire suite, configure retries to catch flakiness:

export default defineConfig({
  retries: 2,
});

Retry segments within a test

You can use the toPass() assertion in conjunction with retries to make sure a code block successfully executes before proceeding. For example:

await expect(async () => {
	await page // click button
		.getByRole(“button”, { name: “Click me” })
		.click()
	await expect( // expect text in popup
		page.getByRole(“heading”, { 
          name: “nice click” 
        }).toBeVisible();
}).toPass();

When the button is clicked, if the javascript hasn’t loaded yet, the popup will not be visible. By including ‘toPass’, Playwright will fail the inner block after a timeout and keep retrying until the javascript has loaded and test passes; or until the global test timeout.

Full Speed Ahead

These are just a few optimization strategies I’ve learned over the years and have used to speed up tests. In my experience, these investment compound as the team and codebase grows; they'll thank you for it later (and probably sooner).

Beyond just improving test speed, these optimizations will also

  • Improve developer experience and morale.

  • Reduce CI costs.

  • Save developer time waiting on slow tests.

  • Help teams ship features and products faster.

Now, I've taken these learnings even further through BuildPulse, a team dedicated to making tests run faster and more reliably. We currently offer BuildPulse Runners: Run your GitHub Actions jobs 2x fast, at half cost, with no tooling changes and BuildPulse Flaky Tests: a platform to detect, monitor, and contain test instability.

If you're tired of slow, buggy CI holding your team back, you can try BuildPulse for free!


FAQ

Does BuildPulse replace my current CI system?

No.

We use GitHub Actions / CircleCI / Semaphore CI self-hosted functionality to run your builds on our infrastructure.

Other than faster builds, there are no changes to tooling or your developer workflows. You can continue using your CI system as-is.

How is BuildPulse faster than GitHub Actions hosted runners?

We use GitHub’s self-hosted functionality to run your builds on our infrastructure with latest generation + high single-core performance CPUs, also then further optimized for CI-type workloads. We’ve also tuned our VMs and block storage devices, increasing baseline performance while also cutting costs in half.

We also provide a toolkit to further speed up your pipelines, which includes ultra fast remote docker builders, docker layer caching, dependency caching, and more. With all of these improvements, we’ve seen 2x+ performance improvements in build times.

Can I use BuildPulse with other CI providers than GitHub Actions?

Yes! BuildPulse Runners will run jobs for CircleCI, SemaphoreCI - GitLab coming soon.

We aim to support all popular CI systems. If you're using one that's not listed, please contact support@buildpulse.io!

Is there a free trial available?

Yes, you can book a meeting here!

How do you secure my builds?

BuildPulse runs each job in a network- and compute- isolated environment with ephemeral VMs that leave behind a clean state after every run.

Do you support Mac and Windows runners?

This is on our roadmap! Email us at hello@buildpulse.io, or book a demo here!

Is BuildPulse SOC 2 compliant?

Yes, BuildPulse is SOC 2 Type 2 compliant.

Contact us at hello@buildpulse.io for more information.

How are BuildPulse Runners priced?

BuildPulse Runners charges on a per-second basis, which depend on the runner-type used. See our pricing page for more details.

How long does implementation/integration with BuildPulse take?

The minimum implementation involves 2 steps: Signing up for BuildPulse, and changing 1 in your GitHub Actions yaml file.

If you're using Semaphore CI or Circle CI, it's a 4 line change. See our Quickstart guide for more details.

Does BuildPulse replace my current CI system?

No.

We use GitHub Actions / CircleCI / Semaphore CI self-hosted functionality to run your builds on our infrastructure.

Other than faster builds, there are no changes to tooling or your developer workflows. You can continue using your CI system as-is.

How is BuildPulse faster than GitHub Actions hosted runners?

We use GitHub’s self-hosted functionality to run your builds on our infrastructure with latest generation + high single-core performance CPUs, also then further optimized for CI-type workloads. We’ve also tuned our VMs and block storage devices, increasing baseline performance while also cutting costs in half.

We also provide a toolkit to further speed up your pipelines, which includes ultra fast remote docker builders, docker layer caching, dependency caching, and more. With all of these improvements, we’ve seen 2x+ performance improvements in build times.

Can I use BuildPulse with other CI providers than GitHub Actions?

Yes! BuildPulse Runners will run jobs for CircleCI, SemaphoreCI - GitLab coming soon.

We aim to support all popular CI systems. If you're using one that's not listed, please contact support@buildpulse.io!

Is there a free trial available?

Yes, you can book a meeting here!

How do you secure my builds?

BuildPulse runs each job in a network- and compute- isolated environment with ephemeral VMs that leave behind a clean state after every run.

Do you support Mac and Windows runners?

This is on our roadmap! Email us at hello@buildpulse.io, or book a demo here!

Is BuildPulse SOC 2 compliant?

Yes, BuildPulse is SOC 2 Type 2 compliant.

Contact us at hello@buildpulse.io for more information.

How are BuildPulse Runners priced?

BuildPulse Runners charges on a per-second basis, which depend on the runner-type used. See our pricing page for more details.

How long does implementation/integration with BuildPulse take?

The minimum implementation involves 2 steps: Signing up for BuildPulse, and changing 1 in your GitHub Actions yaml file.

If you're using Semaphore CI or Circle CI, it's a 4 line change. See our Quickstart guide for more details.

Does BuildPulse replace my current CI system?

No.

We use GitHub Actions / CircleCI / Semaphore CI self-hosted functionality to run your builds on our infrastructure.

Other than faster builds, there are no changes to tooling or your developer workflows. You can continue using your CI system as-is.

How is BuildPulse faster than GitHub Actions hosted runners?

We use GitHub’s self-hosted functionality to run your builds on our infrastructure with latest generation + high single-core performance CPUs, also then further optimized for CI-type workloads. We’ve also tuned our VMs and block storage devices, increasing baseline performance while also cutting costs in half.

We also provide a toolkit to further speed up your pipelines, which includes ultra fast remote docker builders, docker layer caching, dependency caching, and more. With all of these improvements, we’ve seen 2x+ performance improvements in build times.

Can I use BuildPulse with other CI providers than GitHub Actions?

Yes! BuildPulse Runners will run jobs for CircleCI, SemaphoreCI - GitLab coming soon.

We aim to support all popular CI systems. If you're using one that's not listed, please contact support@buildpulse.io!

Is there a free trial available?

Yes, you can book a meeting here!

How do you secure my builds?

BuildPulse runs each job in a network- and compute- isolated environment with ephemeral VMs that leave behind a clean state after every run.

Do you support Mac and Windows runners?

This is on our roadmap! Email us at hello@buildpulse.io, or book a demo here!

Is BuildPulse SOC 2 compliant?

Yes, BuildPulse is SOC 2 Type 2 compliant.

Contact us at hello@buildpulse.io for more information.

How are BuildPulse Runners priced?

BuildPulse Runners charges on a per-second basis, which depend on the runner-type used. See our pricing page for more details.

How long does implementation/integration with BuildPulse take?

The minimum implementation involves 2 steps: Signing up for BuildPulse, and changing 1 in your GitHub Actions yaml file.

If you're using Semaphore CI or Circle CI, it's a 4 line change. See our Quickstart guide for more details.

Ready for Takeoff?

Ready for Takeoff?

Ready for Takeoff?