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:

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.

Terminal output showing a custom Playwright network latency report identifying slow API endpoints
Terminal output showing a custom Playwright network latency report identifying slow API endpoints

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

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.