Skip to main content
Back to blog
tools 8 March 2022 5 min read

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.

M

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:

MetricJMeter/k6Playwright
Server response timeYesYes
JavaScript executionNoYes
DOM renderingNoYes
Core Web VitalsNoYes
Visual renderingNoYes
Third-party script impactLimitedYes

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:

BrowsersApproximate Memory
1200-500MB
51-2GB
102-4GB
204-8GB

Playwright isn’t designed for thousands of concurrent users like k6 or JMeter. Instead, use it for:

  1. Synthetic monitoring: Run periodically to track performance over time
  2. Performance regression testing: Compare metrics between releases
  3. 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:

#playwright #browser-testing #performance-testing #web-vitals

Need help with performance testing?

Let's discuss how I can help improve your application's performance.

Get in Touch