Playwright for performance testing
Playwright is the most capable browser-automation library available for scripting user journeys and capturing front-end performance metrics. This page explains how to run your existing Playwright scripts in MaxoPerf, what Playwright measures, and how to capture Core Web Vitals in a script. It also covers how to pair a small number of Playwright browsers with MaxoPerf’s scale-out protocol load for backend capacity testing.
Before you start
Section titled “Before you start”- Read Browser vs protocol load to learn what each approach is for.
- Read Frontend web vitals under load for the combined pattern.
Run your existing Playwright project in MaxoPerf
Section titled “Run your existing Playwright project in MaxoPerf”MaxoPerf runs Playwright natively with the playwright executor. Each virtual user is a real browser, and every browser gets video, step, HAR and console capture. You don’t have to rewrite your suite:
-
Upload your script as the entrypoint. Any file name works, and so do specs with
test()blocks, specs using your own fixtures file, and library scripts that callchromium.launch()directly. You can also reference the script from a Taurus YAML withexecutor: playwright. -
Upload your
playwright.config.*with it (optional). MaxoPerf wraps the config instead of replacing it. Yourtimeout,retries,useand reporters are kept.testMatchis pinned to your entrypoint, and one project runs per selected browser. -
Pick browsers. Chrome, Edge and Firefox are supported, alone or together. Your config’s
projectspreselect them. Virtual users are split evenly across the browsers you pick. WebKit and mobile projects are skipped with a warning. -
Start the run. On managed locations, MaxoPerf picks a runner size from the number of browsers each runner hosts. If the run fails, it names a cause and shows the engine’s own error text.
The full rules, with tables for config merging, project mapping, load splitting, runner size and failure causes, are in Browser tests → Playwright tests.
Browser VUs for UX, protocol VUs for scale
Section titled “Browser VUs for UX, protocol VUs for scale”A few hundred real browsers are expensive compared with protocol virtual users. A common pattern uses Playwright as the UX measurement layer and protocol load for scale:
- You write a Playwright script that navigates user journeys and collects Core Web Vitals using the
web-vitalsJS library orwindow.performanceAPIs. - You run that script on a small number of browsers, either as a MaxoPerf browser test or locally or in CI, to get accurate front-end timing measurements.
- You run MaxoPerf protocol load (JMeter, k6, or Taurus HTTP) at the same time or one after the other, to stress the backend at scale.
- You compare the two: does the Playwright-captured LCP degrade when the backend is under MaxoPerf protocol load?
This combination gives you both UX truth from the browsers and scale capacity from protocol load, without paying for hundreds of browsers.
Avoid networkidle in load scripts
Section titled “Avoid networkidle in load scripts”waitUntil: 'networkidle' waits until the page has had no network connections for at least 500 ms. Pages with analytics, bot protection, payment widgets or live chat often never get there, so every iteration can time out and nothing is measured. MaxoPerf warns about each networkidle in an uploaded script. Wait for load and then for the element that proves the page is usable:
await page.goto('https://app.example.com/dashboard', { waitUntil: 'load' });await page.getByTestId('run-list-item').first().waitFor();Capturing Core Web Vitals in a Playwright script
Section titled “Capturing Core Web Vitals in a Playwright script”The web-vitals JavaScript library is the standard way to measure LCP, CLS, INP, FID, and TTFB in a real browser. Inject it into your Playwright script with page.addInitScript:
import { chromium, type Page } from 'playwright';
interface WebVitalEntry { name: string; value: number; rating: 'good' | 'needs-improvement' | 'poor';}
async function collectWebVitals(page: Page): Promise<WebVitalEntry[]> { const vitals: WebVitalEntry[] = [];
// Inject web-vitals library before navigation await page.addInitScript(() => { // @ts-ignore — injected at runtime window.__webVitals = []; const script = document.createElement('script'); script.src = 'https://unpkg.com/web-vitals@3/dist/web-vitals.attribution.iife.js'; script.onload = () => { const wv = (window as any).webVitals; ['onLCP', 'onCLS', 'onINP', 'onTTFB', 'onFCP'].forEach(fn => { wv[fn]((metric: any) => { (window as any).__webVitals.push({ name: metric.name, value: metric.value, rating: metric.rating, }); }); }); }; document.head.appendChild(script); });
return vitals;}
async function measureDashboardLoad() { const browser = await chromium.launch({ headless: true }); const page = await browser.newPage();
await collectWebVitals(page);
// Navigate to the target const navStart = Date.now(); await page.goto('https://app.example.com/dashboard', { waitUntil: 'load' }); await page.waitForSelector('[data-testid="run-list-item"]'); const navEnd = Date.now();
// Trigger an interaction to flush INP await page.click('[data-testid="run-list-item"]:first-child'); await page.waitForSelector('[data-testid="run-detail"]');
// Give web-vitals library time to flush final metrics await page.waitForTimeout(500);
// Retrieve collected metrics const webVitals = await page.evaluate(() => (window as any).__webVitals ?? []);
console.log('Navigation timing:', navEnd - navStart, 'ms'); console.log('Core Web Vitals:'); webVitals.forEach((v: WebVitalEntry) => { console.log(` ${v.name}: ${Math.round(v.value)} ms (${v.rating})`); });
await browser.close(); return webVitals;}
measureDashboardLoad();Capturing navigation timing directly
Section titled “Capturing navigation timing directly”For simpler measurements, window.performance.getEntriesByType('navigation') gives you TTFB, DOM content loaded, and load event without an external library:
import { chromium } from 'playwright';
async function captureNavigationTiming(url: string) { const browser = await chromium.launch({ headless: true }); const page = await browser.newPage();
await page.goto(url, { waitUntil: 'load' });
const timing = await page.evaluate(() => { const [entry] = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[]; return { ttfb: entry.responseStart - entry.requestStart, domContentLoaded: entry.domContentLoadedEventEnd - entry.startTime, loadComplete: entry.loadEventEnd - entry.startTime, }; });
console.log('TTFB:', timing.ttfb.toFixed(0), 'ms'); console.log('DOMContentLoaded:', timing.domContentLoaded.toFixed(0), 'ms'); console.log('Load complete:', timing.loadComplete.toFixed(0), 'ms');
await browser.close(); return timing;}Pairing Playwright with MaxoPerf protocol load
Section titled “Pairing Playwright with MaxoPerf protocol load”The most useful pattern is to run Playwright (for web vitals) while MaxoPerf applies backend load (for scale). It tells you whether LCP degrades under API pressure.
-
Set up your MaxoPerf protocol load test. Create a JMeter or k6 test that targets your API endpoints at realistic concurrency (100–500 VUs). Schedule it as a MaxoPerf run.
-
Write a Playwright web-vitals script that navigates to the pages most likely to slow down under API pressure (dashboard, search results, checkout).
-
Run both at the same time. Start the MaxoPerf protocol load run first and wait for steady state (past the ramp-up). Then run the Playwright script from your CI or local machine.
-
Record the Playwright output (TTFB, LCP, CLS values) alongside the MaxoPerf run timestamp.
-
Compare. Run the Playwright script again when the MaxoPerf load has finished. Compare TTFB and LCP between “under load” and “idle” states. An LCP increase of > 20% under load is worth investigating.
Running Playwright in CI alongside MaxoPerf
Section titled “Running Playwright in CI alongside MaxoPerf”A typical CI pipeline that gates on both backend performance and web vitals:
# .github/workflows/perf-gate.yml (illustrative)jobs: protocol-load: runs-on: ubuntu-latest steps: - name: Trigger MaxoPerf protocol load run run: | RUN_ID=$(curl -s -X POST https://app.maxoperf.io/v1/runs \ -H "Authorization: Bearer $MAXOPERF_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"testId": "${{ vars.LOAD_TEST_ID }}"}' | jq -r '.id') echo "RUN_ID=$RUN_ID" >> $GITHUB_ENV
web-vitals-check: runs-on: ubuntu-latest needs: protocol-load steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20' } - run: npm ci - run: npx playwright install chromium - name: Measure web vitals under load run: node scripts/measure-web-vitals.js env: TARGET_URL: https://staging.example.comWhat Playwright does not replace
Section titled “What Playwright does not replace”Playwright is good at measuring web vitals on a handful of browsers. It does not replace:
- MaxoPerf protocol load for generating the concurrent API requests that stress the backend.
- Real-user monitoring (RUM) in production, which collects web vitals from actual users across diverse devices and networks.
Do / don’t
Section titled “Do / don’t”Do:
- Use Playwright for web-vitals measurement alongside MaxoPerf protocol load. Each covers what the other cannot.
- Run Playwright scripts headless in CI to catch front-end regressions before they reach production.
- Capture LCP, CLS, and TTFB under simulated backend load to see the user experience at scale.
Don’t:
- Use hundreds of browser VUs for load that protocol VUs can generate far more cheaply. Keep browsers for the UX measurement.
- Ignore LCP/TTFB degradation under load just because the backend’s p95 API latency looks acceptable.
- Replace protocol load with browser VUs. Browsers measure UX, and protocol load measures capacity.
- Wait for
networkidlein a load script; it may never settle.
Where to go next
Section titled “Where to go next”- Frontend web vitals under load: the full pattern for measuring Core Web Vitals while the backend is stressed.
- Selenium performance testing: running browser VUs inside MaxoPerf using Python WebDriver.
- Hybrid load architecture: combining MaxoPerf browser VUs + protocol VUs in one run.
- Browser tests → Playwright tests: how MaxoPerf runs your Playwright script, config and browsers.