Playwright Performance Testing: What It Can and Cannot Do

Playwright performance testing measures one browser journey: navigation and resource timings, paint events, lab LCP, Chromium runtime metrics, and trace evidence. It can enforce a repeatable engineering budget. It cannot create representative server load or prove field Core Web Vitals by itself, so use k6 browser or Artillery for concurrency, and the web-vitals library for field data from real users.
If you want browser performance checks without building the harness yourself, Qodex writes and runs Playwright performance tests on every pull request from a one-line description.
This page is part of our UI testing guide.
Which tool answers which performance question?
| Question | Use | Evidence returned |
|---|---|---|
| Did one controlled browser journey stay inside its timing budget? | Playwright plus the browser Performance API | Navigation, resource, paint, custom mark and observed LCP timing |
| What is Chromium reporting about the page runtime? | Playwright plus a Chrome DevTools Protocol session | Current runtime counters and durations from Chrome's Performance domain; Chromium only |
| What happened around a slow or failed browser action? | Playwright trace | Timeline, screenshots, DOM snapshots, calls, logs, console, network, metadata and attachments |
| Did a repeatable page audit miss its build assertion? | Lighthouse CI | Lighthouse results plus the configured build assertions |
| What did real users experience? | web-vitals in the application | LCP, INP, CLS, FCP and TTFB measured to match Chrome reporting |
| What happens under concurrent browser traffic? | k6 browser or Artillery's Playwright engine | Virtual-user scenarios with aggregated browser and page metrics |
The web-vitals row describes what the GoogleChrome project says its library does, and the last row describes what Grafana and Artillery say their own tools do. Those are project and vendor capability statements, not benchmarks run for this article.
What Playwright performance testing means
Three different questions hide behind the phrase performance testing, and they need different tools.
The first is how long one browser journey took. The second is what real users experienced across many visits, on their own devices and networks. The third is how a server behaves when many people arrive at the same time.
Playwright answers the first directly. It drives a real browser, so it can read the browser's own timing data during a journey you control, and it can fail a test when a number moves the wrong way. For the other two it is only the automation layer. Field measurement needs code running in your users' browsers. Load generation needs a tool that models virtual users and arrival rates, which is what k6 browser and Artillery add.
What Playwright can measure
page.evaluate() runs a JavaScript function inside the page and brings the result back to the test, and browser globals such as window, document and performance are available inside it. That is the route to the browser Performance API entries below. Playwright's request events and HAR capture are separate APIs.
Navigation timing. A test can read
performance.getEntriesByType('navigation')insidepage.evaluate()to get the document's navigation timing.Resource timing.
performance.getEntriesByType('resource')returns resource timing entries, which is where you look for the script, image or API call behind a slow load.Paint entries.
performance.getEntriesByType('paint')gives first paint and first contentful paint. Checkly's Playwright performance guide shows working code for each of these APIs, plus layout shift and long tasks.An LCP candidate. A
PerformanceObserverwatchinglargest-contentful-paintentries reports the last candidate seen during the run.Custom marks, network events and HAR. Your own
performance.mark()calls land in the same entry list, Playwright exposes request and response events, and it can record a HAR file of the session.
Define LCP once, because the rest of this page leans on it. Google defines Largest Contentful Paint as the render time of the largest image, text block or video visible in the viewport, relative to when the user first navigated.
Now the caveat. A raw PerformanceObserver records LCP candidates, and Google warns that measuring LCP in JavaScript is more complicated than taking the last candidate in every case. That is why the example below is a lab budget rather than a field verdict.
Traces and Chromium runtime counters are two more sources of evidence, and they get their own section.
What Playwright cannot measure alone
Three things sit outside a single Playwright run, and each fails in its own way.
One lab run is not a field Core Web Vitals result. Google's good LCP target is 2.5 seconds or less at the 75th percentile of page loads, segmented across mobile and desktop devices. A single run on one machine cannot produce a 75th percentile, and it cannot stand in for the devices your users actually hold. It can tell you that a change made the page slower on identical hardware, which is a narrower and still useful claim. If the vocabulary here feels blurry, load, stress, and performance testing separates the terms.
Playwright is not a complete load generator. BrowserStack's Playwright performance guide says outright that Playwright is not meant for simulating thousands of virtual users. Grafana's k6 browser module and Artillery's Playwright engine both wrap the same browser automation in workload controls: virtual users, arrival rates, scenarios and aggregated metrics. That those controls ship as separate products is the clearest sign that raising the Playwright worker count is not a workload model. Read that last sentence as an inference from their documented feature sets, not as something Playwright itself says.
Server capacity is a separate measurement. A browser test exercises one client. Throughput, error rates under load and the point where a service saturates come from a load tool aimed at the service, which is what API load testing covers.
Runnable example: measure LCP and assert a page-timing budget
One example carries this page. It captures an LCP candidate and the page load time, requires that an LCP was actually observed, and fails on two explicit budgets. Add Playwright to an existing project and install the Chromium build:
npm init playwright@latest
npx playwright install chromium
Save this as tests/performance.spec.ts:
import { expect, test } from '@playwright/test';
test('homepage stays inside lab performance budgets', async ({ page, browserName }) => {
test.skip(browserName !== 'chromium', 'The LCP PerformanceObserver example is Chromium-only');
await page.addInitScript(() => {
const state = window as typeof window & { __lastLCP?: number };
state.__lastLCP = 0;
new PerformanceObserver((list) => {
const entries = list.getEntries();
const last = entries.at(-1);
if (last) state.__lastLCP = last.startTime;
}).observe({ type: 'largest-contentful-paint', buffered: true });
});
await page.goto(process.env.BASE_URL ?? 'https://example.com', {
waitUntil: 'load',
});
await page.evaluate(
() => new Promise<void>((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
),
);
const metrics = await page.evaluate(() => {
const navigation = performance.getEntriesByType(
'navigation',
)[0] as PerformanceNavigationTiming;
const state = window as typeof window & { __lastLCP?: number };
return {
lcpMs: state.__lastLCP ?? 0,
loadMs: navigation.loadEventEnd - navigation.startTime,
};
});
expect(metrics.lcpMs).toBeGreaterThan(0);
expect(metrics.lcpMs).toBeLessThanOrEqual(2_500);
expect(metrics.loadMs).toBeLessThanOrEqual(3_000);
});
The order matters more than any single line. addInitScript installs the observer before navigation, so it is already listening when the first paint happens; register it after goto and you race the page. The test then loads the target and waits two animation frames, which lets the browser finish the paint that produced the candidate. Only then does it read lcpMs and loadMs out of the page.
The expect() calls do two different jobs. toBeGreaterThan(0) is the guard: it fails when no LCP was observed at all, so a broken observer cannot pass as a fast page. The other two are the budgets. For the full matcher list behind them, see the Playwright assertions reference.
About those numbers. 2,500 ms is Google's good LCP field threshold, and this test borrows it only as a lab engineering budget. The real target is 2.5 seconds or less at the 75th percentile of page loads, split by mobile and desktop, and one run cannot produce that. The 3,000 ms load-event budget is an example value a team would set for itself. No standard recommends it. Run it against an environment you own:
BASE_URL=https://your-preview.example npx playwright test \
--project=chromium tests/performance.spec.ts
This is the boundary in practice. The test above is a timing check on one journey. Playwright is not a load-testing tool, and when the question becomes concurrency the answer is k6 browser or Artillery, covered further down.
Diagnose a regression with traces and Chromium runtime metrics
When a timing check fails, the number tells you nothing about why. Two sources do.
Traces come first, because they need one config line:
trace: 'on-first-retry',
Playwright Trace Viewer then exposes an action timeline, screenshots, DOM snapshots, source, calls, logs, errors, console output, network requests, metadata and attachments for that run. You can scrub to the slow action and see what the page looked like, what it had loaded, and what it was waiting on. Playwright advises recording traces for CI failures and warns that recording one for every test is performance heavy, which is why the setting above only fires on a retry. A trace is diagnostic evidence, not a Web Vital and not a load result. That framing is an inference from the documented feature list rather than a Playwright claim.
Chromium runtime metrics are the second source, and the limitation comes before the technique: browserContext.newCDPSession(page) works only with Chromium-based browsers. Three calls get you the data. Create the session against the page, send Performance.enable to start collecting, then send Performance.getMetrics to retrieve the current values of the run-time metrics from Chrome's Performance domain.
What comes back is a set of Chromium runtime counters and durations. They help when a regression lives in the browser process rather than in your markup. Do not report them as Core Web Vitals, and do not compare them against a Firefox or WebKit run, because there is nothing there to compare against.
Choose between Playwright, web-vitals, Lighthouse CI, k6 browser and Artillery
The table near the top is the short answer. Here is each row as a job rather than a feature list.
Playwright is the right tool when the question is about one controlled browser journey: did this flow stay inside its timing budget, and what does the trace say when it did not. It gives you a repeatable lab number and evidence to debug with.
The web-vitals library is the right tool when the question is what real users experienced. The GoogleChrome project describes it as a modular library for measuring Web Vitals on real users in a way that matches how Chrome measures and reports them, covering LCP, INP, CLS, FCP and TTFB. That is the project's own capability claim, and it is the only one of these tools that produces field data.
Lighthouse CI is the right tool when the question is whether a page still passes a repeatable audit. It runs Lighthouse during a repository build and supports assertions that can fail the build when a score or an audit misses its configured level.
k6 browser and Artillery are the right tools when concurrency and aggregate distributions are the question, which is where the next section goes.
None of these is better than the others. They answer different questions, so which one you reach for follows from the question you have. For a wider survey of the field, see performance testing tools.
Get load numbers with k6 browser or Artillery
Two tools take Playwright-style browser automation and put a workload around it. Both descriptions below come from their own documentation.
Grafana says the k6 browser module adds browser automation and frontend performance metrics to k6. It keeps k6's core load-testing features while it does that. Grafana also documents that the module needs a Chromium-based browser, so the same engine limit that applies to a CDP session applies here.
Artillery says its Playwright engine runs Playwright functions for virtual users, and creates a browser context for each virtual user by default. It reports browser and page metrics including TTFB, FCP, LCP and CLS. Artillery also documents an option to launch a separate browser for every virtual user. It says that option uses much more CPU and memory and is not recommended for most tests, which is a useful warning about what real-browser load costs.
What both add over plain Playwright is the workload model: virtual users, arrival rates, named scenarios, thresholds, and load metrics aggregated across those virtual users rather than one journey's numbers. Pick by the workload you need to describe and by what your team already runs.
Make timing checks stable in CI
A timing number moves on its own unless you hold the conditions still. BrowserStack's guidance on making these checks reliable comes down to four levers, in this order.
Fix the conditions. Pin the browser version and keep the hardware the same from run to run. Hold the network and CPU conditions steady too, so a change in the number means a change in the page.
Decide cold or warm, and stick to it. A cold cache and a warm cache are two different measurements. What matters is that every run in a series measures the same one.
Repeat, then compare percentiles. Run critical checks several times and compare percentiles across runs rather than single numbers.
Set thresholds last. Keep mobile budgets separate from desktop ones. Collect a baseline over several runs before any of it becomes a hard gate.
Keep the honest version in view while you do it. Google's good LCP is 2.5 seconds or less at the 75th percentile, split by mobile and desktop. Your CI job enforces one lab run, which is a narrower claim. If what you want is a repeatable page audit rather than a custom journey timing, Lighthouse CI fits better. For the wider setup around all of this, see the UI performance testing steps.
Turn a browser check into pull-request evidence
The timing assertion above is ordinary Playwright code. You write it, you own it, and nothing about it needs a vendor.
Qodex sits next to that, and the part that matters for a timing budget is the replay. A scenario is saved as standard Playwright, parameterized per environment and exportable, so a timing assertion you add to it is your code rather than a vendor's. Replays run that code with no model in the loop, so the same journey runs the same way every time and a before-and-after comparison means something. Every pull request runs the suite against its preview. When a step fails, it comes back with the failing step, the failing request and a screenshot, and it is classified first: a real bug, a stale test proposed as a diff you approve, or an environment problem such as a preview that did not boot, which is flagged and not counted.
See how Qodex runs UI tests on every pull request.
Frequently Asked Questions about Playwright performance testing
Can Playwright be used for performance testing?
Yes, for one controlled browser journey. It reads navigation, resource and paint timings out of the page, observes an LCP candidate, enforces a lab budget, and keeps a trace on failure. It is not a complete load generator, so it answers how fast this flow was, not how the system holds up under traffic. The detail is in what Playwright can measure.
Is Playwright a load-testing tool?
No. BrowserStack's guide says it is not meant for simulating thousands of virtual users. For virtual users, arrival rates and aggregated load metrics, use Grafana's k6 browser module or Artillery's Playwright engine, both of which document those controls themselves. Playwright supplies the browser automation those tools drive. More in get load numbers with k6 browser or Artillery.
How do I measure LCP in Playwright?
Install a buffered PerformanceObserver for largest-contentful-paint before navigation using addInitScript, load the page, let it settle, then read the last observed candidate back through page.evaluate(). Treat the result as a lab value, since Google warns that measuring LCP in JavaScript has edge cases beyond the last candidate. The full test is in the runnable example.
Can Playwright measure all Core Web Vitals?
Not as a blanket yes. Raw browser APIs give useful lab signals inside a controlled run, which is worth having. Google's web-vitals library is the tool built for field measurement, matching how Chrome measures and reports LCP, INP, CLS, FCP and TTFB on real users. This is covered in what Playwright cannot measure alone.
How do I read performance metrics with page.evaluate()?
page.evaluate() runs a JavaScript function inside the page and returns the result to your test, with window, document and performance available. Call performance.getEntriesByType('navigation') or 'resource' inside it, and return a plain object your assertions can read rather than a live browser entry. The examples are in what Playwright can measure.
When should I use a Chrome DevTools Protocol session?
When you want Chromium's own runtime counters and durations. Create a session with browserContext.newCDPSession(page), send Performance.enable, then Performance.getMetrics. CDP sessions only work on Chromium-based browsers, so keep those numbers separate and never present them as cross-browser Web Vitals. More in traces and Chromium runtime metrics.
Should I use k6 browser or Artillery with Playwright?
Both add a workload model around browser automation. k6 browser keeps core k6 features and needs a Chromium-based browser. Artillery's Playwright engine runs your Playwright code per virtual user, creating a browser context each time by default, and reports TTFB, FCP, LCP and CLS. Choose by workload model and existing stack, as set out in the load section.
How do I run stable Playwright performance checks in CI?
Fix the environment, pin browser versions, decide whether you measure cold or warm loads, repeat each check, and compare percentiles instead of single numbers. Collect a baseline before any threshold becomes a hard gate. When the job is a repeatable page audit, Lighthouse CI assertions fit better. The four levers are in make timing checks stable in CI.
Use Playwright to measure and diagnose one browser journey, field data for what real users get, Lighthouse CI for repeatable audits, and k6 browser or Artillery when concurrency is the question.





