When people think of Playwright, they usually think of end-to-end (E2E) functional testing. But after spending a few months optimizing a heavy SaaS dashboard, I discovered that using playwright for performance testing tutorial style workflows can bridge the gap between ‘it works’ and ‘it’s fast’.
While Playwright isn’t a load-testing tool—for that, you’ll want to follow my grafana k6 load test tutorial—it is incredibly powerful for synthetic performance monitoring. It allows you to measure exactly how a real user experiences your page load, including the time it takes for the Largest Contentful Paint (LCP) to trigger or how long a specific API call takes to resolve.
Prerequisites
Before we dive in, ensure you have the following set up in your environment:
- Node.js (v16 or higher)
- Playwright installed (
npm init playwright@latest) - Basic familiarity with TypeScript or JavaScript
- A target application to test (if you’re working on a frontend framework, check out my react app performance testing guide for framework-specific tips)
Step 1: Capturing Basic Page Load Metrics
The simplest way to start performance testing in Playwright is by leveraging the performance.mark and performance.measure APIs available in the browser. I prefer this method because it gives me millisecond-precision on specific user actions.
import { test, expect } from '@playwright/test';
test('measure home page load performance', async ({ page })
await page.goto('https://your-app.com');
// Execute script within the browser context to get Navigation Timing
const performanceTiming = await page.evaluate(() => {
const timing = window.performance.timing;
return {
pageLoadTime: timing.loadEventEnd - timing.navigationStart,
domContentLoaded: timing.domContentLoadedEventEnd - timing.navigationStart,
connectTime: timing.connectEnd - timing.connectStart,
};
});
console.log(`Page Load Time: ${performanceTiming.pageLoadTime}ms`);
expect(performanceTiming.pageLoadTime).toBeLessThan(3000); // Fail if > 3s
});
Step 2: Monitoring Network Requests and Latency
Functional tests often ignore the “weight” of a page. To truly optimize, you need to know which assets are slowing you down. Playwright’s request and response events allow us to build a custom performance interceptor.
test('analyze network latency for API calls', async ({ page }) => {
const networkLogs = [];
page.on('requestfinished', async (request) => {
const timing = request.timing();
networkLogs.push({
url: request.url(),
duration: timing.responseEnd - timing.requestStart,
});
});
await page.goto('/dashboard');
const slowRequests = networkLogs.filter(log => log.duration > 500);
if (slowRequests.length > 0) {
console.warn('Slow network requests detected:', slowRequests);
}
});
As shown in the output of such tests, identifying a single 2MB image or a hanging API call is often the “low hanging fruit” that solves 80% of performance issues.
Step 3: Integrating Lighthouse for Core Web Vitals
While timing APIs are great, they don’t give you the SEO-critical Core Web Vitals. To get these, I integrate the Lighthouse NPM package with Playwright. This allows me to launch the browser via Playwright, pass the port to Lighthouse, and generate a full audit report.
import lighthouse from 'lighthouse';
import { chromium } from 'playwright';
test('run lighthouse audit', async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://your-app.com');
// Get the browser's remote debugging port
const { port } = await browser.launchServer();
const options = {
logLevel: 'info',
output: 'json',
port: port
};
const result = await lighthouse('https://your-app.com', options);
const score = result.lhs.overall_score * 100;
console.log(`Lighthouse Score: ${score}`);
await browser.close();
expect(score).toBeGreaterThan(80);
});
Pro Tips for Accurate Results
- Disable Caching: Always use
page.context().clearCookies()or launch with a fresh context to avoid skewed results from cached assets. - Throttling: Real users aren’t on 1Gbps fiber. Use
page.route` or Chrome DevTools Protocol (CDP) to simulate "Fast 3G" or "Slow 3G" connections. - Headless vs Headed: I've noticed slight differences in rendering times between headless and headed modes. For performance benchmarks, stick to one and be consistent.
- Consistent Environment: Run these tests on a dedicated CI runner. Running a performance test on a laptop while a Zoom call is happening in the background will produce useless data.
Troubleshooting Common Issues
Issue: Flaky performance results in CI.
Solution: Performance metrics are inherently noisy. Instead of testing for a hard limit (e.g., < 2000ms), I recommend taking the average of 5 runs or using a percentage threshold compared to a baseline.
Issue: Lighthouse fails to connect to the Playwright port.
Solution: Ensure you are using browser.launchServer() and passing that specific port to the Lighthouse configuration, rather than relying on the default 9222.
What's Next?
Now that you can measure page loads, the next step is Performance Budgeting. Set hard limits in your CI pipeline that block a PR if the bundle size increases by more than 5% or if the LCP exceeds 2.5 seconds. If you're dealing with massive scale, I highly suggest moving from synthetic tests to a full-scale load test using k6 to see how your infrastructure holds up under pressure.