Rafał Podraza
Back to blog

June 12, 2026

How to Make Playwright Tests Up to 50% Faster – Practical Tips

Playwright is already a very fast tool for test automation, but test suites can still become slow as the project grows. In this article, I’ll show practical ways to speed up Playwright tests: parallel execution, reusing authenticated sessions, removing unnecessary timeouts, preparing test data through API, improving locators, and optimizing your CI pipeline.

How to Make Playwright Tests Up to 50% Faster – Practical Tips

Playwright is a fast and powerful tool for test automation. The problem usually starts when the project grows, more and more tests are added, and a test suite that once took one minute suddenly takes 10, 15, or even 30 minutes.

And then the classic question appears:

Can we make it faster?

Yes, we can.

Of course, I don’t want to promise that every project will magically become exactly 50% faster. It depends on the application, the test environment, the number of tests, the quality of the tests, and the CI/CD configuration.

But in practice, very often the problem is not that Playwright is slow.

The tests are slow because we do a few things in a non-optimal way.

The most common issues are:

  • logging in before every single test,
  • using artificial waitForTimeout,
  • running tests one by one,
  • not using parallel execution,
  • tests depending on each other,
  • preparing test data through the UI instead of API,
  • running all tests on all browsers after every small change,
  • huge end-to-end tests that check too many things at once.

In this article, I’ll show several practical ways to make Playwright tests much faster.


1. First, measure what actually takes the most time

Before you start optimizing anything, you should first check where you are really losing time.

This is important because it is easy to think: “Playwright is slow.” But after a short analysis, it often turns out that the real problem is somewhere else.

For example:

  • login,
  • creating test data,
  • waiting for API responses,
  • navigating through too many screens,
  • repeating the same steps in every test.

You can start your tests normally:

npx playwright test

Then open the HTML report:

npx playwright show-report

In the report, you can quickly see which tests take the longest. This is where you should start.

There is no point in optimizing a test that takes 2 seconds if another test takes 45 seconds and performs login, account creation, several forms, and maybe even email verification.

First, find the biggest time losses. Then optimize the details.


2. Enable parallel test execution

One of the biggest advantages of Playwright is that tests can be executed in parallel.

This means that tests do not have to run one after another. Playwright can run several test processes at the same time. In larger test suites, this can make a huge difference.

An example playwright.config.ts configuration could look like this:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',

  fullyParallel: true,

  workers: process.env.CI ? 4 : undefined,

  retries: process.env.CI ? 1 : 0,

  reporter: 'html',

  use: {
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});

The most important options here are:

fullyParallel: true

and:

workers: process.env.CI ? 4 : undefined

fullyParallel allows Playwright to run tests fully in parallel. workers defines how many processes can run at the same time.

If you have 100 tests and each test takes only a few seconds, parallel execution can significantly reduce the total execution time.

But there is one important thing to remember:

Parallel tests must be independent.

If test B works only because test A created some data before it, then parallel execution can cause random failures.

A good rule is:

Each test should prepare its own data or use isolated test data.

Tests should not depend on the order in which they are executed.


3. Do not log in before every test

This is one of the most common reasons why Playwright tests become unnecessarily slow.

Imagine you have 80 tests. And every test starts with the same steps:

  1. open the login page,
  2. enter email,
  3. enter password,
  4. click the login button,
  5. wait for the dashboard,
  6. only then start the actual test.

If login takes 5 seconds, then with 80 tests you lose more than 6 minutes just on login steps.

And in most cases, you are not really testing login in every test. Login is only a condition required to access the application.

A better approach is to save the authenticated state once and reuse it in other tests.

You can create a setup file, for example:

import { test as setup, expect } from '@playwright/test';

setup('login', async ({ page }) => {
  await page.goto('/login');

  await page.getByLabel('Email').fill('test@example.com');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Login' }).click();

  await expect(page.getByText('Dashboard')).toBeVisible();

  await page.context().storageState({ path: 'playwright/.auth/user.json' });
});

Then you can use this state in the configuration:

use: {
  storageState: 'playwright/.auth/user.json',
}

Thanks to this, your tests start as an already logged-in user.

This can save a lot of time, especially in applications where login is slow, contains redirects, requires MFA, loads a dashboard, or downloads a large amount of data after authentication.

Of course, you should still have separate tests for login itself. But it usually makes no sense to test login again in every single scenario.


4. Remove artificial timeouts

If you see something like this in the project:

await page.waitForTimeout(3000);

you probably found a good candidate for optimization.

Artificial timeouts are convenient, but they often slow tests down for no good reason. If you have 10 such timeouts in one test, each waiting 3 seconds, you just added 30 seconds of waiting. Even if the application was ready after half a second.

Instead, it is better to use locators and assertions that automatically wait for the correct state.

Instead of:

await page.click('#save');
await page.waitForTimeout(3000);
await expect(page.locator('.success')).toBeVisible();

write:

await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Saved successfully')).toBeVisible();

Playwright can automatically wait until an element is visible, ready, and actionable. You do not need to guess whether the application needs 1, 2, or 3 seconds.

I would leave artificial timeouts only for special cases. For example, when you really want to test behavior after a specific amount of time.

But as a standard way of “stabilizing” tests, they are usually a bad idea.


5. Do not run everything on every browser every time

Playwright allows you to run tests in Chromium, Firefox, and WebKit. This is a great feature.

But you do not always need to run every test on every browser after every small change.

If you run the entire test suite on three browsers for every pull request, the execution time can naturally become much longer.

A better approach can look like this:

  • on every pull request, run a fast test suite only in Chromium,
  • once a day, run the full regression suite on Chromium, Firefox, and WebKit,
  • before a production release, run the full suite on all browsers.

In the configuration, you can define multiple projects:

projects: [
  {
    name: 'chromium',
    use: { ...devices['Desktop Chrome'] },
  },
  {
    name: 'firefox',
    use: { ...devices['Desktop Firefox'] },
  },
  {
    name: 'webkit',
    use: { ...devices['Desktop Safari'] },
  },
]

But in a fast pipeline, you can run only Chromium:

npx playwright test --project=chromium

This is a very practical compromise. You still have cross-browser testing, but you do not waste time running full regression after every small change.


6. Prepare test data through API, not through UI

This is another common issue.

Let’s say you want to test editing an order. Many people would do it like this:

  1. log in,
  2. go to the create order form,
  3. fill in the form,
  4. save the order,
  5. go to the order list,
  6. open order details,
  7. only then test editing.

This can work, but it is slow.

Even worse, if creating the order through the UI fails, your edit test will also fail. Even though the edit functionality itself may still work correctly.

A better approach is to prepare the required data through API.

Instead of clicking through several screens, you send a request, create the data, and immediately start the actual test.

Example:

test('user can edit order', async ({ page, request }) => {
  const response = await request.post('/api/orders', {
    data: {
      productId: 1,
      quantity: 2,
      customerName: 'Test Customer',
    },
  });

  const order = await response.json();

  await page.goto(`/orders/${order.id}`);

  await page.getByRole('button', { name: 'Edit' }).click();
  await page.getByLabel('Quantity').fill('3');
  await page.getByRole('button', { name: 'Save' }).click();

  await expect(page.getByText('Order updated')).toBeVisible();
});

Now the test focuses on what it is supposed to check: editing an order.

You do not waste time clicking through the application only to prepare test data.


7. Separate smoke tests from full regression

Not every test has to run every time.

This is especially important in larger projects. If you have 300 end-to-end tests, running all of them after every small change may simply be impractical.

That is why it is useful to group tests:

  • smoke,
  • regression,
  • critical path,
  • visual,
  • API,
  • e2e.

For example, you can mark a test like this:

test('user can login @smoke', async ({ page }) => {
  await page.goto('/login');

  await page.getByLabel('Email').fill('test@example.com');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Login' }).click();

  await expect(page.getByText('Dashboard')).toBeVisible();
});

Then you can run only smoke tests:

npx playwright test --grep @smoke

The full regression suite can run separately, for example at night or before a release.

This approach works very well in CI/CD. Developers get quick feedback about the most important functionality, while the full regression can run in a separate stage.


8. Be careful with huge end-to-end tests

Not every scenario has to be one large end-to-end test.

Sometimes I see tests that do everything:

  • login,
  • create a customer,
  • create an order,
  • make a payment,
  • check an invoice,
  • trigger shipping,
  • cancel the order,
  • check an email,
  • check the history.

Such a test may make sense as one critical business scenario. But if the whole project consists of tests like this, the suite will become slow, unstable, and difficult to maintain.

It is usually better to have fewer long end-to-end tests and more shorter tests that check specific functionality.

For example:

  • one test for login,
  • one test for creating an order,
  • one test for editing an order,
  • one test for cancelling an order,
  • one test for user permissions.

I would keep long scenarios only for truly critical business paths.

Smaller tests are faster, easier to debug, and usually more stable.


9. Limit video, screenshots, and traces

Playwright can record videos, take screenshots, and save traces. This is very useful for debugging, especially in CI.

But if you save everything, always, for every test, it can slow down execution and generate a lot of unnecessary files.

Instead of:

use: {
  video: 'on',
  trace: 'on',
  screenshot: 'on',
}

it is often better to use:

use: {
  video: 'retain-on-failure',
  trace: 'on-first-retry',
  screenshot: 'only-on-failure',
}

This way, diagnostic materials are saved mostly when they are actually needed.

It is a good compromise. You still have enough information to analyze failures, but you do not add extra work to every successful test.


10. Set reasonable timeouts

Timeouts are necessary, but values that are too high can hide problems.

If every test can wait 60 seconds for an element, a failing test may hang for a long time before it finally fails.

An example configuration could look like this:

export default defineConfig({
  timeout: 30 * 1000,

  expect: {
    timeout: 5000,
  },

  use: {
    actionTimeout: 10000,
    navigationTimeout: 15000,
  },
});

The goal is not to set extremely low timeouts. The goal is to make them reasonable.

If your application usually loads a screen in 2 seconds, waiting 60 seconds for every element usually does not make sense.

It is better to detect the problem faster and then fix the test or the application.


11. Do not overuse beforeEach

beforeEach is very useful, but it can quickly become a place where too much happens.

For example:

test.beforeEach(async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('test@example.com');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Login' }).click();
  await page.goto('/dashboard');
  await page.getByRole('button', { name: 'Accept cookies' }).click();
});

If this code runs before every test, it quickly becomes expensive.

Sometimes it is better to:

  • use storageState,
  • prepare data through API,
  • go directly to the required URL,
  • avoid repeating the same steps,
  • move shared logic into fixtures.

A good beforeEach should prepare only what is really needed for the specific test.


12. Use sharding for large test suites

If you have a large number of tests, you can split them into multiple parts. This is called sharding.

Example:

npx playwright test --shard=1/4
npx playwright test --shard=2/4
npx playwright test --shard=3/4
npx playwright test --shard=4/4

This allows you to distribute tests across multiple machines or CI jobs.

It makes sense especially when the project becomes larger and increasing the number of workers on one machine is no longer enough.

In practice, instead of one pipeline running for 20 minutes, you can have several parallel jobs that finish much faster.


13. Improve your locators

Bad locators can make tests slower and less stable.

If tests often use complicated CSS selectors or XPath, they are usually harder to maintain. They also break more easily when the HTML structure changes.

Instead of:

await page.locator('div:nth-child(3) > button').click();

use:

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

Or:

await page.getByTestId('save-button').click();

Good locators do not only improve readability. They also reduce situations where Playwright has to wait too long or accidentally finds the wrong element.

I usually use:

  • getByRole,
  • getByLabel,
  • getByText,
  • getByPlaceholder,
  • getByTestId.

A test should be readable. When someone opens a test file, they should roughly understand what is happening without analyzing the entire HTML structure.


14. Do not test everything through the UI

This may sound a bit controversial, but not everything should be a UI test.

End-to-end tests are valuable, but they are also expensive. They start a browser, click through the application, wait for rendering, communicate with the backend, and often depend on many external systems.

That is why it is worth asking:

Does this case really have to be tested through the UI?

Sometimes other types of tests are better:

  • unit tests,
  • integration tests,
  • API tests,
  • component tests,
  • a few E2E tests for critical paths.

A good testing strategy is not about automating everything through the browser. It is about checking functionality at the right level.

If you can test form validation properly at the component level, you do not always need a full end-to-end test for that.


15. Example of a faster Playwright configuration

Here is an example configuration that can be a good starting point:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',

  fullyParallel: true,

  timeout: 30 * 1000,

  expect: {
    timeout: 5000,
  },

  retries: process.env.CI ? 1 : 0,

  workers: process.env.CI ? 4 : undefined,

  reporter: [['html'], ['list']],

  use: {
    baseURL: 'https://example.com',

    actionTimeout: 10000,
    navigationTimeout: 15000,

    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },

  projects: [
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/user.json',
      },
    },
  ],
});

This is not a perfect configuration for every project.

I would treat it more as a starting point. In a real project, you need to adjust workers, timeouts, browsers, test data, and CI settings to your application and infrastructure.


What usually gives the biggest speed improvement?

If I had to choose the most important things, they would be:

  1. running tests in parallel,
  2. reusing an authenticated session,
  3. removing waitForTimeout,
  4. preparing test data through API,
  5. separating smoke tests from full regression,
  6. limiting the number of browsers in fast pipelines,
  7. making end-to-end tests smaller and more focused.

In many projects, the first three points are already enough to make a noticeable difference.

If every test previously logged in from scratch and also contained several artificial timeouts, the improvement can be really significant.


Summary

Making Playwright tests faster is not about one magic configuration option.

It is a combination of several good decisions:

  • write independent tests,
  • run tests in parallel,
  • do not repeat login in every test,
  • avoid artificial timeouts,
  • prepare data through API,
  • do not run full regression after every small change,
  • do not test everything through the UI.

In my opinion, this is one of the most important topics in test automation.

Tests should not only work. They should also provide fast feedback.

If tests take too long, the team eventually stops treating them as help. They start treating them as an obstacle.

Well-written Playwright tests should be fast, stable, and easy to understand. That is when they really help improve software quality.

Most popular courses

If this topic is useful to you, these courses are a strong next step.