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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "performance-testing" with this command: npx skills add adaptationio/skrillz/adaptationio-skrillz-performance-testing

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Security

eks-security

No summary provided by upstream source.

Repository SourceNeeds Review
Security

security-sandbox

No summary provided by upstream source.

Repository SourceNeeds Review
Security

ac-security-sandbox

No summary provided by upstream source.

Repository SourceNeeds Review
General

finnhub-api

No summary provided by upstream source.

Repository SourceNeeds Review