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.
Playwright for Performance Testing Tutorial: 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.
Official resources for Performance Testing
Documentation and references for Performance Testing: