Your Playwright scripts pass. They cover the right flows. But somewhere between a working test and a working pipeline, the suite turned into something that takes forever to run and costs more CI minutes than it should. Optimizing Playwright scripts for performance testing is not about adding more tooling. It is about fixing the specific things that are quietly wasting time in every run.
This post covers the actual levers. Parallel execution, context reuse, smart filtering, tracing, and network simulation. Not as a checklist to implement all at once, but as a ranked set of interventions you apply based on where your suite is losing time right now.

What Is Actually Slowing Your Playwright Scripts Down
Before touching config, it helps to know what the real culprits are. Most slow Playwright suites have the same three problems. Tests run sequentially when they do not need to. New browser instances spin up for every test when they could share a context. And hardcoded waits or waitForTimeout calls are scattered throughout the suite like little time bombs that add seconds to every run.
None of these are Playwright problems. They are configuration and authoring problems. Playwright gives you the tools to fix all of them out of the box. The question is whether you have used them.
Parallel Execution: Fix This First
If your tests are running sequentially, this is the single highest-leverage change you can make to optimize Playwright scripts. Playwright supports parallel workers natively through playwright.config.ts. Setting the workers property to match your available CPU cores cuts total execution time proportionally.
ts
// playwright.config.ts
export default {
workers: 4, // match to available CPU cores in your CI environment
use: {
headless: true,
},
};The practical constraint here is test isolation. Parallel execution only works cleanly when tests do not share state. If your tests depend on the same user session, the same database record, or the same test data, parallelism will cause flakiness that looks random but is not. Fix the isolation problem first, then increase workers. The correct order matters.
Browser Context Reuse: The Thing Most Teams Skip
Creating a new browser instance for every test is the second most common source of unnecessary overhead in Playwright suites. Browser launches are expensive. Context creation is cheap. The difference in execution time compounds across a large suite.
ts
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://your-app.com');
// run your test interactions here
await browser.close();The pattern here is launching the browser once, creating a context, and reusing that context across interactions rather than relaunching for every flow. In test fixtures, this means setting up the browser at the suite level and tearing it down once rather than per-test. Playwright’s built-in test.beforeAll and test.afterAll hooks make this straightforward to implement without restructuring your tests.
Smart Filtering: Stop Running Everything Every Time
Not every test needs to run on every commit. Playwright supports tag-based filtering so you can define which tests are performance-critical and run only those in your fast feedback loop.
bash
npx playwright test --grep "@critical"In practice this means tagging your highest-risk flows with @critical and configuring CI to run only that subset on pull requests, with the full suite running on merge or nightly. The result is faster feedback on the changes that matter most without sacrificing coverage on the builds that can afford the time. If you are running the full suite on every commit and your pipeline is slow, this is a quick win that does not require touching a single test.
Headless Mode and Wait Mechanisms
Headless mode is the default for good reason. Running browsers without rendering the UI is faster, and in CI there is no display context anyway. The flag is straightforward.
ts
const browser = await chromium.launch({ headless: true });The wait mechanism issue is more nuanced. Hardcoded delays with waitForTimeout are the most common cause of artificial slowness in Playwright suites. They add fixed time regardless of whether the element is ready. Replace them with smart waits that resolve as soon as the condition is true.
ts
// instead of this
await page.waitForTimeout(3000);
// do this
await page.waitForSelector('#target-element');The selector-based wait exits the moment the element appears. The timeout wait burns the full three seconds every time. Across a suite of any size, the difference is significant, and the selector approach is also more reliable because it is tied to actual application state rather than an arbitrary number someone picked when the test was flaky.
Performance Metrics and Tracing: What Playwright Already Gives You
Playwright exposes browser-level performance metrics without additional tooling. page.metrics() returns data on JavaScript heap size, DOM node count, layout duration, and other signals that surface performance problems before they become user-facing.
ts
const metrics = await page.metrics();
console.log(metrics);Tracing is the more powerful tool for diagnosing specific bottlenecks. When you enable tracing on a context, Playwright records screenshots and DOM snapshots at each step. The trace viewer lets you replay the execution and see exactly where time is being spent.
ts
await context.tracing.start({ screenshots: true, snapshots: true });
// run your test
await context.tracing.stop({ path: 'trace.zip' });Then view it with:
bash
npx playwright show-trace trace.zipIf your scripts are slow and you do not know why, this is where to look. The trace viewer will show you which step is taking the longest. That answer is almost always more useful than guessing.
Network Simulation: Testing What Real Users Actually Experience
Playwright lets you simulate specific network conditions during test execution. This matters for performance testing because your suite running on a fast CI machine with a direct database connection is not the same as a user on a mobile connection in a region with high latency.
ts
await context.setNetworkConditions({
offline: false,
downloadThroughput: 500 * 1024, // 500 KBps
uploadThroughput: 128 * 1024, // 128 KBps
latency: 200, // 200ms
});This is particularly useful for testing checkout flows, file uploads, and any interaction where network speed affects user-perceived performance. A test that passes under ideal conditions and fails under simulated real-world conditions is a valid failure. It means you found something before your users did.
A Complete Optimized Script
Here is what an optimized Playwright performance test looks like when you apply the patterns above together.
ts
import { chromium } from 'playwright';
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1280, height: 720 },
});
// enable tracing before interactions
await context.tracing.start({ screenshots: true, snapshots: true });
const page = await context.newPage();
await page.goto('https://your-app.com');
// smart waits instead of hardcoded delays
await page.waitForSelector('#username');
await page.fill('#username', 'test_user');
await page.fill('#password', 'secure_password');
await page.click('#login');
// capture metrics after key interaction
const metrics = await page.metrics();
console.log('JS Heap Used:', metrics.JSHeapUsedSize);
// stop tracing and save
await context.tracing.stop({ path: 'trace.zip' });
await browser.close();
})();This script launches once, reuses the context, uses smart waits, captures metrics, and traces the execution. It is not doing anything exotic. It is applying the defaults Playwright already supports, consistently.
What to Stop Doing
Most Playwright performance problems come from a short list of authoring habits that are worth naming directly. Overloading a single test file with unrelated flows means the whole file runs when only one flow changed. Not updating performance baselines after application changes means you are comparing against numbers that no longer reflect reality. Ignoring CI environment constraints means a suite that runs fine locally hits memory limits in the pipeline.
None of these are hard to fix. They are habits, and habits change when you have a clearer picture of what they are costing you. The patterns in this post are the picture. The fixing is straightforward once you know where to look.
If you are also building out a broader browser automation pipeline with Playwright beyond just testing, the post on building a Playwright browser automation pipeline covers how to extend these same patterns into scheduled automation and scraping workflows.





0 thoughts on “How to Optimize Playwright Scripts for Performance Testing”