Performance Testing with Playwright: Beyond Functional Testing
Learn how to use Playwright for performance testing, measuring page load times, Core Web Vitals, and identifying frontend bottlenecks.
Mark
Performance Testing Expert
Playwright is primarily known as a functional testing tool, but its capabilities extend well into performance testing territory. Unlike protocol-level tools like JMeter or k6, Playwright runs a real browser, making it ideal for measuring what users actually experience.
Why Playwright for Performance?
Traditional load testing tools send HTTP requests directly to servers. They’re excellent for measuring server-side performance but miss crucial aspects of the user experience:
| Metric | JMeter/k6 | Playwright |
|---|---|---|
| Server response time | Yes | Yes |
| JavaScript execution | No | Yes |
| DOM rendering | No | Yes |
| Core Web Vitals | No | Yes |
| Visual rendering | No | Yes |
| Third-party script impact | Limited | Yes |
For applications where frontend performance matters—and it usually does—Playwright fills a gap that protocol-level testing cannot.
Setting Up Playwright
Install Playwright with its test runner:
npm init playwright@latest
For performance-focused testing, I prefer the library mode over the test runner:
npm install playwright
Measuring Page Load Metrics
Playwright provides access to the Performance API through the browser context:
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
// Navigate and wait for network idle
await page.goto('https://example.com', { waitUntil: 'networkidle' });
// Extract performance timing
const timing = await page.evaluate(() => {
const perf = performance.timing;
return {
dns: perf.domainLookupEnd - perf.domainLookupStart,
tcp: perf.connectEnd - perf.connectStart,
ttfb: perf.responseStart - perf.requestStart,
download: perf.responseEnd - perf.responseStart,
domInteractive: perf.domInteractive - perf.navigationStart,
domComplete: perf.domComplete - perf.navigationStart,
loadEvent: perf.loadEventEnd - perf.navigationStart,
};
});
console.log('Performance Metrics:', timing);
await browser.close();
})();
Capturing Core Web Vitals
Core Web Vitals are Google’s user-centric performance metrics. Playwright can capture all three:
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
// Inject web-vitals library
await page.addInitScript(() => {
window.webVitals = { lcp: null, fid: null, cls: null };
});
// Listen for LCP
page.on('console', msg => {
if (msg.text().startsWith('LCP:')) {
console.log(msg.text());
}
});
await page.goto('https://example.com');
// Measure Largest Contentful Paint
const lcp = await page.evaluate(() => {
return new Promise(resolve => {
new PerformanceObserver(list => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
resolve(lastEntry.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });
});
});
// Measure Cumulative Layout Shift
const cls = await page.evaluate(() => {
return new Promise(resolve => {
let clsValue = 0;
new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
}
resolve(clsValue);
}).observe({ type: 'layout-shift', buffered: true });
// Resolve after a delay to capture shifts
setTimeout(() => resolve(clsValue), 3000);
});
});
console.log(`LCP: ${lcp}ms, CLS: ${cls}`);
await browser.close();
})();
Throttling Network and CPU
To simulate real-world conditions, throttle network and CPU:
const context = await browser.newContext();
const page = await context.newPage();
// Create a CDP session for throttling
const client = await context.newCDPSession(page);
// Simulate 3G network
await client.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: (750 * 1024) / 8, // 750 Kbps
uploadThroughput: (250 * 1024) / 8, // 250 Kbps
latency: 100, // 100ms RTT
});
// Throttle CPU (4x slowdown)
await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });
await page.goto('https://example.com');
This helps identify performance issues that only appear on slower devices or connections.
Capturing Network Requests
Monitor all network requests to identify slow resources:
const requests = [];
page.on('request', request => {
requests.push({
url: request.url(),
method: request.method(),
startTime: Date.now(),
});
});
page.on('response', response => {
const request = requests.find(r => r.url === response.url());
if (request) {
request.duration = Date.now() - request.startTime;
request.status = response.status();
request.size = response.headers()['content-length'];
}
});
await page.goto('https://example.com', { waitUntil: 'networkidle' });
// Find slowest requests
const slowest = requests
.filter(r => r.duration)
.sort((a, b) => b.duration - a.duration)
.slice(0, 10);
console.log('Slowest requests:', slowest);
Visual Performance with Screenshots
Capture screenshots at key moments to understand visual loading:
await page.goto('https://example.com', { waitUntil: 'commit' });
await page.screenshot({ path: 'first-paint.png' });
await page.waitForLoadState('domcontentloaded');
await page.screenshot({ path: 'dom-loaded.png' });
await page.waitForLoadState('networkidle');
await page.screenshot({ path: 'network-idle.png' });
This visual timeline helps identify render-blocking resources and layout shifts.
Running Performance Tests at Scale
For load testing with Playwright, you’re limited by browser resource requirements. A single browser instance uses significant memory:
| Browsers | Approximate Memory |
|---|---|
| 1 | 200-500MB |
| 5 | 1-2GB |
| 10 | 2-4GB |
| 20 | 4-8GB |
Playwright isn’t designed for thousands of concurrent users like k6 or JMeter. Instead, use it for:
- Synthetic monitoring: Run periodically to track performance over time
- Performance regression testing: Compare metrics between releases
- User journey timing: Measure complete user flows including client-side rendering
For high-volume load testing, combine Playwright with protocol-level tools:
// k6 for load, Playwright for user experience
// Run k6 load test
// Simultaneously run Playwright to measure UX under load
Integration with CI/CD
Add performance budgets to your pipeline:
const { test, expect } = require('@playwright/test');
test('homepage meets performance budget', async ({ page }) => {
await page.goto('https://example.com');
const timing = await page.evaluate(() => ({
lcp: performance.getEntriesByType('largest-contentful-paint')[0]?.startTime,
domComplete: performance.timing.domComplete - performance.timing.navigationStart,
}));
expect(timing.lcp).toBeLessThan(2500); // LCP under 2.5s
expect(timing.domComplete).toBeLessThan(3000); // DOM complete under 3s
});
Playwright complements rather than replaces traditional performance testing tools. Use it when you need to understand the complete user experience, including everything that happens after the server responds. For pure server-side load testing, stick with k6 or JMeter.
Tags: