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.

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.

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: