CtrlK
BlogDocsLog inGet started
Tessl Logo

performance-testing

Performance testing with Lighthouse and Web Vitals integration in Playwright. Use when measuring page load times, Core Web Vitals, Lighthouse audits, or performance budgets.

60

Quality

70%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Fix and improve this skill with Tessl

tessl review fix ./skills/performance-testing/SKILL.md
SKILL.md
Quality
Evals
Security

Performance Testing with Lighthouse & Web Vitals

Measure and enforce performance standards using Lighthouse audits and Core Web Vitals within Playwright tests.

Quick Start

import { test, expect } from '@playwright/test';
import { playAudit } from 'playwright-lighthouse';

test('homepage meets performance budget', async ({ page }) => {
  await page.goto('/');

  const audit = await playAudit({
    page,
    thresholds: {
      performance: 90,
      accessibility: 90,
      'best-practices': 90,
      seo: 90,
    },
  });

  expect(audit.lhr.categories.performance.score * 100).toBeGreaterThanOrEqual(90);
});

Installation

npm install -D playwright-lighthouse lighthouse

Lighthouse Integration

Basic Audit

import { test, expect } from '@playwright/test';
import { playAudit } from 'playwright-lighthouse';

test('run lighthouse audit', async ({ page }) => {
  await page.goto('/');

  const audit = await playAudit({
    page,
    port: 9222,  // Chrome debug port
  });

  console.log('Performance:', audit.lhr.categories.performance.score * 100);
  console.log('Accessibility:', audit.lhr.categories.accessibility.score * 100);
  console.log('Best Practices:', audit.lhr.categories['best-practices'].score * 100);
  console.log('SEO:', audit.lhr.categories.seo.score * 100);
});

With Thresholds

test('enforce performance budgets', async ({ page }) => {
  await page.goto('/');

  const audit = await playAudit({
    page,
    thresholds: {
      performance: 85,
      accessibility: 90,
      'best-practices': 85,
      seo: 80,
    },
  });

  // Test fails if any threshold is not met
});

Mobile vs Desktop

test('mobile performance', async ({ page }) => {
  await page.goto('/');

  const audit = await playAudit({
    page,
    config: {
      extends: 'lighthouse:default',
      settings: {
        formFactor: 'mobile',
        throttling: {
          rttMs: 150,
          throughputKbps: 1638.4,
          cpuSlowdownMultiplier: 4,
        },
        screenEmulation: {
          mobile: true,
          width: 375,
          height: 667,
          deviceScaleFactor: 2,
        },
      },
    },
  });
});

test('desktop performance', async ({ page }) => {
  await page.goto('/');

  const audit = await playAudit({
    page,
    config: {
      extends: 'lighthouse:default',
      settings: {
        formFactor: 'desktop',
        throttling: {
          rttMs: 40,
          throughputKbps: 10240,
          cpuSlowdownMultiplier: 1,
        },
        screenEmulation: {
          mobile: false,
          width: 1350,
          height: 940,
          deviceScaleFactor: 1,
        },
      },
    },
  });
});

Core Web Vitals

Measure Web Vitals

import { test, expect } from '@playwright/test';

test('measure Core Web Vitals', async ({ page }) => {
  // Inject web-vitals library
  await page.addInitScript(() => {
    window.webVitals = {
      LCP: null,
      FID: null,
      CLS: null,
      FCP: null,
      TTFB: null,
    };
  });

  await page.goto('/');

  // Wait for metrics to be collected
  await page.waitForTimeout(3000);

  // Get LCP
  const lcp = await page.evaluate(() => {
    return new Promise(resolve => {
      new PerformanceObserver((list) => {
        const entries = list.getEntries();
        resolve(entries[entries.length - 1].startTime);
      }).observe({ type: 'largest-contentful-paint', buffered: true });
    });
  });

  // Get CLS
  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 });
      setTimeout(() => resolve(clsValue), 1000);
    });
  });

  console.log('LCP:', lcp, 'ms');
  console.log('CLS:', cls);

  // Assert thresholds
  expect(lcp).toBeLessThan(2500);  // Good LCP < 2.5s
  expect(cls).toBeLessThan(0.1);   // Good CLS < 0.1
});

Web Vitals Library Integration

test('web vitals with library', async ({ page }) => {
  await page.addInitScript({
    content: `
      import { onLCP, onFID, onCLS, onFCP, onTTFB } from 'web-vitals';

      window.webVitalsResults = {};

      onLCP(metric => window.webVitalsResults.LCP = metric.value);
      onFID(metric => window.webVitalsResults.FID = metric.value);
      onCLS(metric => window.webVitalsResults.CLS = metric.value);
      onFCP(metric => window.webVitalsResults.FCP = metric.value);
      onTTFB(metric => window.webVitalsResults.TTFB = metric.value);
    `
  });

  await page.goto('/');

  // Interact to trigger FID
  await page.click('body');
  await page.waitForTimeout(2000);

  const vitals = await page.evaluate(() => window.webVitalsResults);

  console.log('Web Vitals:', vitals);
});

Performance Timing API

Navigation Timing

test('page load timing', async ({ page }) => {
  await page.goto('/');

  const timing = await page.evaluate(() => {
    const perf = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
    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.fetchStart,
      domComplete: perf.domComplete - perf.fetchStart,
      loadComplete: perf.loadEventEnd - perf.fetchStart,
    };
  });

  console.log('Performance Timing:', timing);

  expect(timing.ttfb).toBeLessThan(600);
  expect(timing.domInteractive).toBeLessThan(3000);
  expect(timing.loadComplete).toBeLessThan(5000);
});

Resource Timing

test('resource loading', async ({ page }) => {
  await page.goto('/');

  const resources = await page.evaluate(() => {
    return performance.getEntriesByType('resource').map(r => ({
      name: r.name,
      type: (r as PerformanceResourceTiming).initiatorType,
      duration: r.duration,
      size: (r as PerformanceResourceTiming).transferSize,
    }));
  });

  // Find slow resources
  const slowResources = resources.filter(r => r.duration > 1000);
  console.log('Slow resources:', slowResources);

  // Find large resources
  const largeResources = resources.filter(r => r.size > 100000);
  console.log('Large resources:', largeResources);
});

Performance Budgets

Define Budgets

const performanceBudgets = {
  // Page load
  ttfb: 600,           // Time to first byte < 600ms
  fcp: 1800,           // First contentful paint < 1.8s
  lcp: 2500,           // Largest contentful paint < 2.5s
  tti: 3800,           // Time to interactive < 3.8s

  // Interactivity
  fid: 100,            // First input delay < 100ms
  cls: 0.1,            // Cumulative layout shift < 0.1

  // Resources
  totalSize: 1000000,  // Total page size < 1MB
  jsSize: 300000,      // JavaScript < 300KB
  cssSize: 100000,     // CSS < 100KB
  imageSize: 500000,   // Images < 500KB

  // Requests
  totalRequests: 50,   // Total requests < 50
  jsRequests: 10,      // JS files < 10
};

test('check performance budgets', async ({ page }) => {
  await page.goto('/');

  // Get resource sizes
  const resources = await page.evaluate(() => {
    const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
    return {
      total: entries.reduce((sum, r) => sum + r.transferSize, 0),
      js: entries.filter(r => r.name.endsWith('.js')).reduce((sum, r) => sum + r.transferSize, 0),
      css: entries.filter(r => r.name.endsWith('.css')).reduce((sum, r) => sum + r.transferSize, 0),
      images: entries.filter(r => r.initiatorType === 'img').reduce((sum, r) => sum + r.transferSize, 0),
      requests: entries.length,
    };
  });

  expect(resources.total).toBeLessThan(performanceBudgets.totalSize);
  expect(resources.js).toBeLessThan(performanceBudgets.jsSize);
  expect(resources.requests).toBeLessThan(performanceBudgets.totalRequests);
});

Network Throttling

Simulate Slow Connections

test('performance on 3G', async ({ page, context }) => {
  const client = await context.newCDPSession(page);

  // Simulate slow 3G
  await client.send('Network.emulateNetworkConditions', {
    offline: false,
    downloadThroughput: (400 * 1024) / 8,  // 400 Kbps
    uploadThroughput: (400 * 1024) / 8,
    latency: 400,
  });

  const startTime = Date.now();
  await page.goto('/');
  const loadTime = Date.now() - startTime;

  console.log('Load time on 3G:', loadTime, 'ms');

  // Should still be usable on slow connections
  expect(loadTime).toBeLessThan(10000);
});

CPU Throttling

test('performance on slow CPU', async ({ page, context }) => {
  const client = await context.newCDPSession(page);

  // 4x CPU slowdown
  await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });

  await page.goto('/');

  // Measure interaction responsiveness
  const startTime = Date.now();
  await page.click('.interactive-element');
  await page.waitForSelector('.result');
  const responseTime = Date.now() - startTime;

  expect(responseTime).toBeLessThan(500);
});

Reporting

Generate HTML Report

import { playAudit } from 'playwright-lighthouse';
import fs from 'fs';

test('generate performance report', async ({ page }) => {
  await page.goto('/');

  const audit = await playAudit({
    page,
    thresholds: { performance: 80 },
  });

  // Save HTML report
  fs.writeFileSync(
    'lighthouse-report.html',
    audit.report
  );

  // Save JSON for further analysis
  fs.writeFileSync(
    'lighthouse-report.json',
    JSON.stringify(audit.lhr, null, 2)
  );
});

Track Metrics Over Time

interface PerformanceMetrics {
  date: string;
  url: string;
  lcp: number;
  fcp: number;
  cls: number;
  performance: number;
}

test('track performance metrics', async ({ page }) => {
  await page.goto('/');

  const audit = await playAudit({ page });

  const metrics: PerformanceMetrics = {
    date: new Date().toISOString(),
    url: page.url(),
    lcp: audit.lhr.audits['largest-contentful-paint'].numericValue,
    fcp: audit.lhr.audits['first-contentful-paint'].numericValue,
    cls: audit.lhr.audits['cumulative-layout-shift'].numericValue,
    performance: audit.lhr.categories.performance.score * 100,
  };

  // Append to metrics file
  const metricsFile = 'performance-history.json';
  const history = fs.existsSync(metricsFile)
    ? JSON.parse(fs.readFileSync(metricsFile, 'utf8'))
    : [];

  history.push(metrics);
  fs.writeFileSync(metricsFile, JSON.stringify(history, null, 2));
});

CI Integration

GitHub Actions

name: Performance Tests

on: [push, pull_request]

jobs:
  performance:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright
        run: npx playwright install --with-deps chromium

      - name: Start app
        run: npm run start &

      - name: Wait for app
        run: npx wait-on http://localhost:3000

      - name: Run performance tests
        run: npx playwright test --grep @performance

      - name: Upload Lighthouse report
        uses: actions/upload-artifact@v4
        with:
          name: lighthouse-report
          path: lighthouse-report.html

Best Practices

  1. Test on realistic conditions - Use network/CPU throttling
  2. Test multiple pages - Home, product, checkout, etc.
  3. Track over time - Compare against baselines
  4. Set budgets early - Prevent regression
  5. Test mobile performance - Often worse than desktop
  6. Cache and repeat - Run multiple times for consistency

References

  • references/web-vitals-guide.md - Understanding Core Web Vitals
  • references/lighthouse-config.md - Custom Lighthouse configurations
Repository
fernandezbaptiste/Skrillz
Last updated
First committed

Also appears in

fernandezbaptiste/Skrillz
In sync

since Sep 12, 2026

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.