Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Playwright testing guide covering E2E, component, API, visual, accessibility, and security tests.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
testing-patterns/performance-testing.md
1# Performance Testing & Web Vitals23## Table of Contents451. [Core Web Vitals](#core-web-vitals)62. [Performance Metrics](#performance-metrics)73. [Performance Budgets](#performance-budgets)84. [Lighthouse Integration](#lighthouse-integration)95. [Performance Fixtures](#performance-fixtures)106. [CI Performance Monitoring](#ci-performance-monitoring)1112## Core Web Vitals1314### Measure LCP, FID, CLS1516```typescript17test("core web vitals within thresholds", async ({ page }) => {18// Inject web-vitals library19await page.addInitScript(() => {20(window as any).__webVitals = {};2122// Simplified web vitals collection23new PerformanceObserver((list) => {24for (const entry of list.getEntries()) {25if (entry.entryType === "largest-contentful-paint") {26(window as any).__webVitals.lcp = entry.startTime;27}28}29}).observe({ type: "largest-contentful-paint", buffered: true });3031new PerformanceObserver((list) => {32let cls = 0;33for (const entry of list.getEntries() as any[]) {34if (!entry.hadRecentInput) {35cls += entry.value;36}37}38(window as any).__webVitals.cls = cls;39}).observe({ type: "layout-shift", buffered: true });40});4142await page.goto("/");4344// Wait for page to stabilize45await page.waitForLoadState("networkidle");4647// Get metrics48const vitals = await page.evaluate(() => (window as any).__webVitals);4950// Assert thresholds (Google's "good" thresholds)51expect(vitals.lcp).toBeLessThan(2500); // LCP < 2.5s52expect(vitals.cls).toBeLessThan(0.1); // CLS < 0.153});54```5556### Using web-vitals Library5758```typescript59test("web vitals with library", async ({ page }) => {60await page.addInitScript(() => {61(window as any).__vitals = {};62});6364// Inject web-vitals after navigation65await page.goto("/");6667await page.addScriptTag({68url: "https://unpkg.com/web-vitals@3/dist/web-vitals.iife.js",69});7071await page.evaluate(() => {72const { onLCP, onFID, onCLS, onFCP, onTTFB } = (window as any).webVitals;7374onLCP((metric: any) => ((window as any).__vitals.lcp = metric.value));75onFID((metric: any) => ((window as any).__vitals.fid = metric.value));76onCLS((metric: any) => ((window as any).__vitals.cls = metric.value));77onFCP((metric: any) => ((window as any).__vitals.fcp = metric.value));78onTTFB((metric: any) => ((window as any).__vitals.ttfb = metric.value));79});8081// Trigger FID by clicking82await page.getByRole("button").first().click();8384// Wait and collect85await page.waitForTimeout(1000);8687const vitals = await page.evaluate(() => (window as any).__vitals);8889console.log("Web Vitals:", vitals);9091// Assertions92if (vitals.lcp) expect(vitals.lcp).toBeLessThan(2500);93if (vitals.fid) expect(vitals.fid).toBeLessThan(100);94if (vitals.cls) expect(vitals.cls).toBeLessThan(0.1);95});96```9798## Performance Metrics99100### Navigation Timing101102```typescript103test("page load performance", async ({ page }) => {104await page.goto("/");105106const timing = await page.evaluate(() => {107const nav = performance.getEntriesByType(108"navigation",109)[0] as PerformanceNavigationTiming;110111return {112// Time to First Byte113ttfb: nav.responseStart - nav.requestStart,114// DOM Content Loaded115domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,116// Full page load117loadComplete: nav.loadEventEnd - nav.startTime,118// DNS lookup119dns: nav.domainLookupEnd - nav.domainLookupStart,120// Connection time121connection: nav.connectEnd - nav.connectStart,122// Download time123download: nav.responseEnd - nav.responseStart,124// DOM processing125domProcessing: nav.domComplete - nav.domInteractive,126};127});128129console.log("Performance timing:", timing);130131// Assertions132expect(timing.ttfb).toBeLessThan(600); // TTFB < 600ms133expect(timing.domContentLoaded).toBeLessThan(2000); // DCL < 2s134expect(timing.loadComplete).toBeLessThan(4000); // Load < 4s135});136```137138### Resource Timing139140```typescript141test("resource loading performance", async ({ page }) => {142await page.goto("/");143144const resources = await page.evaluate(() => {145return performance.getEntriesByType("resource").map((entry) => ({146name: entry.name.split("/").pop(),147type: (entry as PerformanceResourceTiming).initiatorType,148duration: entry.duration,149size: (entry as PerformanceResourceTiming).transferSize,150}));151});152153// Find slow resources154const slowResources = resources.filter((r) => r.duration > 1000);155156if (slowResources.length > 0) {157console.warn("Slow resources:", slowResources);158}159160// Find large resources161const largeResources = resources.filter((r) => r.size > 500000); // > 500KB162163expect(largeResources.length).toBe(0);164});165```166167### Memory Usage168169```typescript170test("memory usage is reasonable", async ({ page }) => {171await page.goto("/dashboard");172173// Check memory (Chrome only)174const memory = await page.evaluate(() => {175if ((performance as any).memory) {176return {177usedJSHeapSize: (performance as any).memory.usedJSHeapSize,178totalJSHeapSize: (performance as any).memory.totalJSHeapSize,179};180}181return null;182});183184if (memory) {185const usedMB = memory.usedJSHeapSize / 1024 / 1024;186console.log(`Memory usage: ${usedMB.toFixed(2)} MB`);187188// Assert reasonable memory usage189expect(usedMB).toBeLessThan(100); // < 100MB190}191});192```193194## Performance Budgets195196### Define Budgets197198```typescript199// performance-budgets.ts200export const budgets = {201homepage: {202lcp: 2500,203cls: 0.1,204fcp: 1800,205ttfb: 600,206totalSize: 1500000, // 1.5MB207jsSize: 500000, // 500KB208imageCount: 20,209},210dashboard: {211lcp: 3000,212cls: 0.1,213fcp: 2000,214ttfb: 800,215totalSize: 2000000,216jsSize: 800000,217},218};219```220221### Test Against Budgets222223```typescript224import { budgets } from "./performance-budgets";225226test("homepage meets performance budget", async ({ page }) => {227const budget = budgets.homepage;228229await page.goto("/");230await page.waitForLoadState("networkidle");231232// Measure LCP233const lcp = await page.evaluate(() => {234return new Promise<number>((resolve) => {235new PerformanceObserver((list) => {236const entries = list.getEntries();237resolve(entries[entries.length - 1].startTime);238}).observe({ type: "largest-contentful-paint", buffered: true });239});240});241242// Measure resources243const resources = await page.evaluate(() => {244const entries = performance.getEntriesByType(245"resource",246) as PerformanceResourceTiming[];247return {248totalSize: entries.reduce((sum, e) => sum + (e.transferSize || 0), 0),249jsSize: entries250.filter((e) => e.initiatorType === "script")251.reduce((sum, e) => sum + (e.transferSize || 0), 0),252imageCount: entries.filter((e) => e.initiatorType === "img").length,253};254});255256// Assert budgets257expect(lcp, "LCP exceeds budget").toBeLessThan(budget.lcp);258expect(resources.totalSize, "Total size exceeds budget").toBeLessThan(259budget.totalSize,260);261expect(resources.jsSize, "JS size exceeds budget").toBeLessThan(262budget.jsSize,263);264expect(resources.imageCount, "Too many images").toBeLessThanOrEqual(265budget.imageCount,266);267});268```269270### Budget Fixture271272```typescript273// fixtures/performance.fixture.ts274type PerformanceBudget = {275lcp?: number;276cls?: number;277ttfb?: number;278totalSize?: number;279};280281type PerformanceFixtures = {282assertBudget: (budget: PerformanceBudget) => Promise<void>;283};284285export const test = base.extend<PerformanceFixtures>({286assertBudget: async ({ page }, use) => {287await use(async (budget) => {288const metrics = await page.evaluate(() => {289const nav = performance.getEntriesByType(290"navigation",291)[0] as PerformanceNavigationTiming;292const resources = performance.getEntriesByType(293"resource",294) as PerformanceResourceTiming[];295296return {297ttfb: nav.responseStart - nav.requestStart,298totalSize: resources.reduce(299(sum, r) => sum + (r.transferSize || 0),3000,301),302};303});304305if (budget.ttfb) {306expect(307metrics.ttfb,308`TTFB ${metrics.ttfb}ms exceeds budget ${budget.ttfb}ms`,309).toBeLessThan(budget.ttfb);310}311312if (budget.totalSize) {313expect(metrics.totalSize, `Total size exceeds budget`).toBeLessThan(314budget.totalSize,315);316}317});318},319});320```321322## Lighthouse Integration323324### Using playwright-lighthouse325326```bash327npm install -D playwright-lighthouse lighthouse328```329330```typescript331import { playAudit } from "playwright-lighthouse";332333test("lighthouse audit", async ({ page }) => {334await page.goto("/");335336// Run Lighthouse337const audit = await playAudit({338page,339port: 9222, // Chrome debugging port340thresholds: {341performance: 80,342accessibility: 90,343"best-practices": 80,344seo: 80,345},346});347348// Assertions349expect(audit.lhr.categories.performance.score * 100).toBeGreaterThanOrEqual(35080,351);352expect(audit.lhr.categories.accessibility.score * 100).toBeGreaterThanOrEqual(35390,354);355});356```357358### Lighthouse with Config359360```typescript361test("lighthouse with custom config", async ({ page }, testInfo) => {362await page.goto("/");363364const audit = await playAudit({365page,366port: 9222,367thresholds: {368performance: 70,369},370config: {371extends: "lighthouse:default",372settings: {373onlyCategories: ["performance"],374throttling: {375rttMs: 40,376throughputKbps: 10240,377cpuSlowdownMultiplier: 1,378},379},380},381});382383// Save report384const reportPath = testInfo.outputPath("lighthouse-report.html");385// Save audit.report to file386387// Attach to test report388await testInfo.attach("lighthouse", {389body: JSON.stringify(audit.lhr),390contentType: "application/json",391});392});393```394395## CI Performance Monitoring396397### Track Performance Over Time398399```typescript400// reporters/perf-reporter.ts401import { Reporter, TestResult } from "@playwright/test/reporter";402403class PerfReporter implements Reporter {404private metrics: any[] = [];405406onTestEnd(test: any, result: TestResult) {407const perfAnnotation = test.annotations.find(408(a: any) => a.type === "performance",409);410411if (perfAnnotation) {412this.metrics.push({413test: test.title,414...JSON.parse(perfAnnotation.description),415timestamp: new Date().toISOString(),416});417}418}419420async onEnd() {421// Send to metrics service422if (process.env.METRICS_ENDPOINT) {423await fetch(process.env.METRICS_ENDPOINT, {424method: "POST",425body: JSON.stringify({426commit: process.env.GITHUB_SHA,427branch: process.env.GITHUB_REF,428metrics: this.metrics,429}),430});431}432}433}434435export default PerfReporter;436```437438### Performance Regression Detection439440```typescript441test("no performance regression", async ({ page }) => {442await page.goto("/");443444const metrics = await page.evaluate(() => {445const nav = performance.getEntriesByType(446"navigation",447)[0] as PerformanceNavigationTiming;448return {449loadTime: nav.loadEventEnd - nav.startTime,450};451});452453// Compare against baseline (could be from file or API)454const baseline = 2000; // ms455const threshold = 1.1; // 10% regression allowed456457expect(458metrics.loadTime,459`Load time ${metrics.loadTime}ms is ${((metrics.loadTime / baseline - 1) * 100).toFixed(1)}% slower than baseline`,460).toBeLessThan(baseline * threshold);461});462```463464## Anti-Patterns to Avoid465466| Anti-Pattern | Problem | Solution |467| --------------------------- | ------------------------- | -------------------------------- |468| Testing only once | Results vary | Run multiple times, use averages |469| Ignoring network conditions | Unrealistic results | Test with throttling |470| No baseline comparison | Can't detect regressions | Track metrics over time |471| Testing in dev mode | Slow, not production-like | Test production builds |472473## Related References474475- **Performance Optimization**: See [performance.md](../infrastructure-ci-cd/performance.md) for test execution performance476- **CI/CD**: See [ci-cd.md](../infrastructure-ci-cd/ci-cd.md) for CI integration477