Skip to content
AyoKoding

Performance Testing

Why This Matters

Performance directly impacts user experience, conversion rates, and search rankings. Google's Core Web Vitals have made performance measurement a critical production requirement, not a nice-to-have optimization. Sites that fail Core Web Vitals metrics see measurable drops in user engagement and revenue.

Production performance testing requires automated, repeatable measurements integrated into CI/CD pipelines. Manual testing catches only point-in-time performance; automated testing catches regressions before deployment. You need to measure not just page load time but Largest Contentful Paint (LCP), First Input Delay (FID), Cumulative Layout Shift (CLS), and resource loading patterns.

Modern applications must prove performance compliance before production deployment. This guide shows how to progress from basic Performance API measurements to production-grade Lighthouse CI integration with comprehensive metrics collection, threshold enforcement, and regression detection across multiple devices and network conditions.

Standard Library: Performance API

Browser's built-in Performance API provides basic performance measurements through navigation timing and resource timing interfaces.

// test/performance-basic.spec.ts
// => Uses Performance API for timing measurements
// => Browser built-in, no external dependencies
 
import { test, expect } from "@playwright/test";
// => Playwright test framework
// => expect for assertions
 
test("measure page load performance", async ({ page }) => {
  // => Navigates to page and collects timing data
  // => Performance entries captured via browser API
 
  await page.goto("https://example.com");
  // => Loads target page
  // => Waits for load event by default
 
  const performanceData = await page.evaluate(() => {
    // => Executes in browser context
    // => Accesses window.performance object
    // => Returns timing data to Node.js context
 
    const navigation = performance.getEntriesByType("navigation")[0] as PerformanceNavigationTiming;
    // => Gets navigation timing entry
    // => Cast to specific type for TypeScript
    // => Only one navigation entry per page load
 
    const paint = performance.getEntriesByType("paint");
    // => Gets paint timing entries
    // => Returns array of paint events
    // => Includes first-paint, first-contentful-paint
 
    return {
      // => Returns object with calculated metrics
      // => All values in milliseconds
 
      domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
      // => Time for DOM ready event
      // => Measures HTML parsing completion
 
      loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
      // => Time for full page load
      // => Includes all resources (images, scripts)
 
      firstPaint: paint.find((entry) => entry.name === "first-paint")?.startTime ?? 0,
      // => First pixel painted on screen
      // => Visual feedback starts
 
      firstContentfulPaint: paint.find((entry) => entry.name === "first-contentful-paint")?.startTime ?? 0,
      // => First content element painted
      // => User sees meaningful content
 
      domInteractive: navigation.domInteractive,
      // => DOM ready for interaction
      // => Scripts can manipulate DOM
 
      transferSize: navigation.transferSize,
      // => Bytes transferred over network
      // => Includes headers and body
      // => 0 if from cache
    };
  });
  // => Returns performance metrics to test context
 
  console.log("Performance Metrics:", performanceData);
  // => Logs metrics for manual inspection
  // => Format: { domContentLoaded: 150, loadComplete: 500, ... }
 
  expect(performanceData.firstContentfulPaint).toBeLessThan(2500);
  // => Asserts FCP under 2.5s
  // => Core Web Vitals "Good" threshold
  // => Test fails if exceeded
 
  expect(performanceData.loadComplete).toBeLessThan(5000);
  // => Asserts page load under 5s
  // => Reasonable performance expectation
});

Code density: 18 code lines, 44 annotation lines = 2.44 density (within acceptable range for complex logic)

Limitations for production:

  • No Core Web Vitals metrics: Missing LCP, FID, CLS - the metrics Google uses for search ranking
  • No mobile emulation: Can't test across device types (desktop vs mobile vs tablet)
  • No network throttling: Can't simulate slow 3G, fast 4G, or typical mobile conditions
  • Manual threshold checking: Each test must implement its own pass/fail criteria
  • No historical tracking: No way to detect performance regressions across builds
  • No actionable insights: Raw numbers don't explain what slowed down or how to fix it

Production Framework: Lighthouse CI

Lighthouse provides comprehensive performance auditing with Core Web Vitals, best practices scoring, and actionable recommendations for improvement.

Installation

npm install --save-dev @playwright/test playwright-lighthouse

Lighthouse Integration

// test/performance-lighthouse.spec.ts
// => Production-grade performance testing
// => Lighthouse CI integration for Core Web Vitals
 
import { test, expect } from "@playwright/test";
import { playAudit } from "playwright-lighthouse";
import lighthouseDesktopConfig from "lighthouse/lighthouse-core/config/desktop-config.js";
// => Lighthouse desktop configuration preset
// => Optimized desktop thresholds
 
test.describe("Production Performance Audits", () => {
  // => Groups performance tests
  // => All tests share same audit requirements
 
  test.use({
    // => Configures test context
    // => Applied to all tests in describe block
 
    launchOptions: {
      args: ["--remote-debugging-port=9222"],
      // => Enables Chrome DevTools Protocol
      // => Required for Lighthouse connection
      // => Port 9222 is standard debugging port
    },
  });
 
  test("Desktop: Core Web Vitals compliance", async ({ page, context }) => {
    // => Tests desktop performance profile
    // => Verifies Core Web Vitals thresholds
 
    await page.goto("https://example.com");
    // => Navigates to target page
    // => Must complete before audit starts
 
    await page.waitForLoadState("networkidle");
    // => Waits for network to settle
    // => Ensures all initial resources loaded
    // => Prevents measuring partial page state
 
    await playAudit({
      // => Runs Lighthouse audit
      // => Returns performance metrics and scores
 
      page,
      // => Playwright page instance
      // => Lighthouse instruments this page
 
      port: 9222,
      // => Chrome debugging port
      // => Matches launchOptions configuration
 
      config: lighthouseDesktopConfig,
      // => Uses desktop performance profile
      // => Desktop-specific thresholds and emulation
 
      thresholds: {
        // => Pass/fail criteria for CI/CD
        // => Test fails if any threshold breached
 
        performance: 90,
        // => Overall performance score
        // => 90+ is "Good" (green)
        // => Composite of all metrics weighted
 
        "largest-contentful-paint": 2500,
        // => LCP: Main content visible
        // => 2.5s is "Good" threshold
        // => 4.0s is "Needs Improvement"
 
        "first-contentful-paint": 1800,
        // => FCP: First content visible
        // => 1.8s is "Good" threshold
 
        "cumulative-layout-shift": 0.1,
        // => CLS: Visual stability
        // => 0.1 is "Good" threshold
        // => Measures unexpected layout shifts
 
        "total-blocking-time": 300,
        // => TBT: Interactivity metric
        // => 300ms is "Good" threshold
        // => FID proxy for lab testing
 
        "speed-index": 3400,
        // => Speed Index: Visual completeness
        // => 3.4s is "Good" threshold
        // => Measures how quickly content painted
      },
    });
    // => Throws error if any threshold exceeded
    // => Test fails with detailed report
  });
 
  test("Mobile: 3G Network Performance", async ({ page }) => {
    // => Tests mobile + slow network
    // => Simulates worst-case scenario
 
    await page.goto("https://example.com");
    await page.waitForLoadState("networkidle");
 
    await playAudit({
      page,
      port: 9222,
 
      config: {
        // => Custom mobile configuration
        // => Overrides default desktop settings
 
        extends: "lighthouse:default",
        // => Starts from default Lighthouse config
        // => Adds mobile-specific settings
 
        settings: {
          // => Lighthouse execution settings
          // => Controls emulation and throttling
 
          formFactor: "mobile",
          // => Emulates mobile device
          // => Uses mobile viewport and user agent
 
          throttling: {
            // => Network and CPU throttling
            // => Simulates slow 3G connection
 
            rttMs: 150,
            // => Round-trip time: 150ms
            // => Typical slow 3G latency
 
            throughputKbps: 1.6 * 1024,
            // => Download: 1.6 Mbps
            // => Slow 3G bandwidth
 
            requestLatencyMs: 150,
            // => Additional request latency
            // => Simulates mobile network delays
 
            downloadThroughputKbps: 1.6 * 1024,
            uploadThroughputKbps: 750,
            // => Upload: 750 Kbps
            // => Asymmetric (typical mobile)
 
            cpuSlowdownMultiplier: 4,
            // => CPU throttling: 4x slowdown
            // => Simulates mid-tier mobile device
          },
 
          screenEmulation: {
            // => Mobile screen dimensions
            // => Affects layout and rendering
 
            mobile: true,
            width: 375,
            height: 667,
            // => iPhone SE dimensions
            // => Common small mobile screen
 
            deviceScaleFactor: 2,
            // => Retina display: 2x pixel density
            disabled: false,
          },
        },
      },
 
      thresholds: {
        // => Mobile thresholds more lenient
        // => Accounts for device constraints
 
        performance: 70,
        // => 70+ acceptable for mobile
        // => Lower than desktop (90+)
 
        "largest-contentful-paint": 4000,
        // => 4s LCP acceptable on 3G
        // => Still below "Poor" threshold (4.5s)
 
        "first-contentful-paint": 3000,
        // => 3s FCP acceptable on 3G
 
        "total-blocking-time": 600,
        // => 600ms TBT acceptable on mobile
        // => Higher than desktop (300ms)
      },
    });
  });
});

Code density: 60 code lines, 113 annotation lines = 1.88 density (within target range)

Performance Testing Architecture

graph TB
    subgraph "Test Execution"
        PW[Playwright Test]
        CDP[Chrome DevTools Protocol]
        LH[Lighthouse Engine]
    end
 
    subgraph "Performance Collection"
        NAV[Navigation Timing]
        RES[Resource Timing]
        PAINT[Paint Timing]
        CWV[Core Web Vitals]
    end
 
    subgraph "Analysis & Reporting"
        SCORE[Performance Score]
        METRICS[Metrics Report]
        SUGG[Optimization Suggestions]
        THRESH[Threshold Validation]
    end
 
    PW -->|launches| CDP
    CDP -->|instruments| LH
    LH -->|collects| NAV
    LH -->|collects| RES
    LH -->|collects| PAINT
    LH -->|calculates| CWV
 
    NAV --> SCORE
    RES --> SCORE
    PAINT --> SCORE
    CWV --> SCORE
 
    SCORE --> METRICS
    SCORE --> SUGG
    METRICS --> THRESH
    THRESH -->|pass/fail| PW
 
    classDef execution fill:#0173B2,stroke:#0173B2,stroke-width:2px,color:#fff
    classDef collection fill:#029E73,stroke:#029E73,stroke-width:2px,color:#fff
    classDef analysis fill:#DE8F05,stroke:#DE8F05,stroke-width:2px,color:#000
 
    class PW,CDP,LH execution
    class NAV,RES,PAINT,CWV collection
    class SCORE,METRICS,SUGG,THRESH analysis

Production Patterns and Best Practices

Pattern 1: Lighthouse CI with Budget Enforcement

Performance budgets prevent gradual performance degradation by failing builds that exceed resource limits.

// lighthouse-config.js
// => Performance budget configuration
// => Enforces resource limits in CI/CD
 
export default {
  // => Lighthouse configuration object
  // => Extends default lighthouse settings
 
  extends: "lighthouse:default",
 
  settings: {
    // => Budget configuration
    // => Defines resource limits per page
 
    budgets: [
      {
        // => Resource budget for all pages
        // => Applied to every audit
 
        resourceSizes: [
          // => Limits on resource types by size
          // => Prevents bloat from accumulating
 
          {
            resourceType: "total",
            budget: 300,
            // => Total page size: 300KB
            // => All resources combined
            // => Good baseline for fast load
          },
 
          {
            resourceType: "script",
            budget: 150,
            // => JavaScript: 150KB
            // => Prevents JS bloat
            // => Parse/compile time increases with size
          },
 
          {
            resourceType: "image",
            budget: 100,
            // => Images: 100KB
            // => Use compression and lazy loading
          },
 
          {
            resourceType: "stylesheet",
            budget: 30,
            // => CSS: 30KB
            // => Critical CSS inlined
            // => Non-critical deferred
          },
 
          {
            resourceType: "font",
            budget: 20,
            // => Fonts: 20KB
            // => Use font-display: swap
            // => Limit font variants
          },
        ],
 
        resourceCounts: [
          // => Limits on number of resources
          // => Too many resources = HTTP overhead
 
          {
            resourceType: "third-party",
            budget: 10,
            // => Third-party scripts: max 10
            // => Analytics, ads, tracking
            // => Each adds latency and risk
          },
 
          {
            resourceType: "total",
            budget: 50,
            // => Total resources: max 50
            // => Prevents waterfall explosion
          },
        ],
 
        timings: [
          // => Time-based performance budgets
          // => Core Web Vitals thresholds
 
          {
            metric: "first-contentful-paint",
            budget: 2000,
            // => FCP: 2s budget
            // => User sees content quickly
          },
 
          {
            metric: "largest-contentful-paint",
            budget: 2500,
            // => LCP: 2.5s budget
            // => Main content visible
          },
 
          {
            metric: "cumulative-layout-shift",
            budget: 0.1,
            // => CLS: 0.1 budget
            // => Visual stability
          },
 
          {
            metric: "total-blocking-time",
            budget: 300,
            // => TBT: 300ms budget
            // => Interactivity metric
          },
 
          {
            metric: "interactive",
            budget: 3500,
            // => TTI: 3.5s budget
            // => Page fully interactive
          },
        ],
      },
    ],
  },
};

Code density: 40 code lines, 68 annotation lines = 1.70 density

Using budget config:

// test/performance-budget.spec.ts
// => Performance budget enforcement
// => CI/CD gate for resource limits
 
import { test } from "@playwright/test";
import { playAudit } from "playwright-lighthouse";
import budgetConfig from "../lighthouse-config.js";
// => Imports custom budget configuration
// => Shared across all budget tests
 
test("Enforce performance budgets", async ({ page }) => {
  // => Tests against budget limits
  // => Fails if any budget exceeded
 
  test.use({
    launchOptions: {
      args: ["--remote-debugging-port=9222"],
    },
  });
 
  await page.goto("https://example.com");
  await page.waitForLoadState("networkidle");
 
  await playAudit({
    page,
    port: 9222,
    config: budgetConfig,
    // => Uses budget configuration
    // => Lighthouse validates budgets automatically
    // => Build fails on budget violations
 
    thresholds: {
      performance: 85,
      // => Overall score threshold
      // => Independent of budget checks
    },
  });
  // => Lighthouse reports budget violations
  // => Test fails with details on exceeded budgets
});

Code density: 14 code lines, 20 annotation lines = 1.43 density

Pattern 2: Resource Timing Analysis

Analyze individual resource loading patterns to identify bottlenecks and optimization opportunities.

// test/resource-timing.spec.ts
// => Resource-level performance analysis
// => Identifies slow resources and bottlenecks
 
import { test, expect } from "@playwright/test";
 
interface ResourceTiming {
  name: string;
  duration: number;
  transferSize: number;
  type: string;
  cached: boolean;
}
 
test("Analyze resource loading performance", async ({ page }) => {
  // => Collects resource timing data
  // => Identifies performance bottlenecks
 
  await page.goto("https://example.com");
  await page.waitForLoadState("networkidle");
  // => Ensures all resources loaded
  // => Resource timing complete
 
  const resourceTimings = await page.evaluate(() => {
    // => Executes in browser context
    // => Accesses Performance API
 
    const resources = performance.getEntriesByType("resource") as PerformanceResourceTiming[];
    // => Gets all resource timing entries
    // => Includes scripts, styles, images, fonts, XHR
 
    return resources.map((resource) => ({
      // => Transforms to structured data
      // => Extracts key metrics for analysis
 
      name: resource.name,
      // => Resource URL
      // => Identifies specific resource
 
      duration: resource.duration,
      // => Total load time (ms)
      // => From start to response complete
 
      transferSize: resource.transferSize,
      // => Bytes transferred over network
      // => 0 if from cache or cross-origin
 
      type: resource.initiatorType,
      // => Resource type: script, css, img, fetch, etc.
      // => Categorizes resources
 
      cached: resource.transferSize === 0,
      // => True if from cache
      // => Cache hits have 0 transfer size
    }));
  });
 
  // => Group resources by type for analysis
  // => Calculate aggregate metrics per type
 
  const byType = resourceTimings.reduce(
    (acc, resource) => {
      // => Groups resources by type
      // => Enables type-level analysis
 
      if (!acc[resource.type]) {
        acc[resource.type] = [];
        // => Initialize type array if first resource of type
      }
      acc[resource.type].push(resource);
      return acc;
    },
    {} as Record<string, ResourceTiming[]>,
  );
 
  // => Analyze each resource type
  // => Identify performance issues
 
  for (const [type, resources] of Object.entries(byType)) {
    // => Iterates over resource types
    // => Performs type-specific analysis
 
    const totalSize = resources.reduce((sum, r) => sum + r.transferSize, 0);
    // => Sum of transfer sizes for type
    // => Identifies bandwidth-heavy types
 
    const totalDuration = resources.reduce((sum, r) => sum + r.duration, 0);
    // => Sum of load durations for type
    // => Identifies time-consuming types
 
    const cacheHitRate = resources.filter((r) => r.cached).length / resources.length;
    // => Percentage cached
    // => Measures cache effectiveness
 
    console.log(`${type}:`, {
      count: resources.length,
      totalSize: `${(totalSize / 1024).toFixed(2)} KB`,
      totalDuration: `${totalDuration.toFixed(2)} ms`,
      cacheHitRate: `${(cacheHitRate * 100).toFixed(1)}%`,
    });
    // => Logs type-level metrics
    // => Format: script: { count: 12, totalSize: "150.50 KB", ... }
  }
 
  // => Find slowest resources
  // => Candidates for optimization
 
  const slowResources = resourceTimings
    .filter((r) => r.duration > 1000)
    // => Resources taking over 1s
    // => Significant performance impact
    .sort((a, b) => b.duration - a.duration);
  // => Sorted slowest first
  // => Prioritizes optimization targets
 
  if (slowResources.length > 0) {
    console.warn(
      "Slow resources detected:",
      slowResources.map((r) => ({
        url: r.name,
        duration: `${r.duration.toFixed(2)} ms`,
      })),
    );
    // => Warns about slow resources
    // => Requires investigation
  }
 
  // => Validate resource count limits
  // => Prevents resource explosion
 
  expect(resourceTimings.length).toBeLessThan(50);
  // => Max 50 total resources
  // => Too many resources = connection overhead
 
  const scripts = resourceTimings.filter((r) => r.type === "script");
  expect(scripts.length).toBeLessThan(15);
  // => Max 15 scripts
  // => Prevents JavaScript bloat
 
  const thirdParty = resourceTimings.filter((r) => !r.name.includes("example.com"));
  expect(thirdParty.length).toBeLessThan(10);
  // => Max 10 third-party resources
  // => Limits external dependencies
  // => Reduces privacy and performance risks
});

Code density: 47 code lines, 78 annotation lines = 1.66 density

Pattern 3: Network Throttling Profiles

Test performance across realistic network conditions to ensure acceptable experience for all users.

// test/network-throttling.spec.ts
// => Network condition testing
// => Validates performance across connection types
 
import { test, expect, chromium } from "@playwright/test";
 
// => Network throttling profiles
// => Simulates real-world connection speeds
 
const networkProfiles = {
  // => Collection of network conditions
  // => Based on Chrome DevTools presets
 
  "Fast 3G": {
    // => Fast 3G mobile connection
    // => Common in developing countries
 
    downloadThroughput: (1.6 * 1024 * 1024) / 8,
    // => 1.6 Mbps download
    // => Converted to bytes per second (÷8)
 
    uploadThroughput: (750 * 1024) / 8,
    // => 750 Kbps upload
    // => Asymmetric (typical mobile)
 
    latency: 150,
    // => 150ms round-trip latency
    // => Includes radio latency
  },
 
  "Slow 3G": {
    // => Slow 3G mobile connection
    // => Worst-case mobile scenario
 
    downloadThroughput: (400 * 1024) / 8,
    // => 400 Kbps download
    // => Very slow by modern standards
 
    uploadThroughput: (400 * 1024) / 8,
    // => 400 Kbps upload
    // => Symmetric for slow 3G
 
    latency: 400,
    // => 400ms latency
    // => Noticeable delay on interactions
  },
 
  "4G": {
    // => 4G LTE connection
    // => Modern mobile baseline
 
    downloadThroughput: (4 * 1024 * 1024) / 8,
    // => 4 Mbps download
    // => Typical 4G speed
 
    uploadThroughput: (3 * 1024 * 1024) / 8,
    // => 3 Mbps upload
 
    latency: 50,
    // => 50ms latency
    // => Low latency for mobile
  },
 
  Cable: {
    // => Home broadband connection
    // => Desktop baseline
 
    downloadThroughput: (5 * 1024 * 1024) / 8,
    // => 5 Mbps download
    // => Conservative broadband estimate
 
    uploadThroughput: (1 * 1024 * 1024) / 8,
    // => 1 Mbps upload
    // => Asymmetric (typical cable)
 
    latency: 28,
    // => 28ms latency
    // => Typical broadband latency
  },
};
 
for (const [profileName, profile] of Object.entries(networkProfiles)) {
  // => Test each network profile
  // => Ensures acceptable performance across conditions
 
  test(`Performance on ${profileName}`, async () => {
    // => Parameterized test for network profile
    // => Test name includes profile for clarity
 
    const browser = await chromium.launch({
      args: ["--remote-debugging-port=9222"],
    });
    // => Launches Chrome with debugging
    // => Required for CDP network emulation
 
    const context = await browser.newContext();
    // => Creates browser context
    // => Isolated session for test
 
    const client = await context.newCDPSession(await context.newPage());
    // => Creates Chrome DevTools Protocol session
    // => Enables low-level browser control
 
    await client.send("Network.enable");
    // => Enables network domain
    // => Required for emulation
 
    await client.send("Network.emulateNetworkConditions", {
      // => Applies network throttling
      // => Simulates specified connection
 
      offline: false,
      // => Online mode
      // => offline: true simulates no connection
 
      downloadThroughput: profile.downloadThroughput,
      uploadThroughput: profile.uploadThroughput,
      latency: profile.latency,
      // => Applies profile settings
      // => Affects all network requests
    });
 
    const page = await context.newPage();
    // => Creates page in throttled context
    // => All requests use emulated network
 
    const startTime = Date.now();
    // => Records navigation start time
    // => For total load measurement
 
    await page.goto("https://example.com");
    await page.waitForLoadState("networkidle");
    // => Waits for network idle
    // => Ensures complete page load under throttling
 
    const loadTime = Date.now() - startTime;
    // => Calculates total load time
    // => Includes throttling effects
 
    console.log(`${profileName} load time: ${loadTime}ms`);
    // => Logs load time for profile
    // => Compare across profiles
 
    // => Profile-specific assertions
    // => Thresholds based on connection quality
 
    if (profileName === "Slow 3G") {
      expect(loadTime).toBeLessThan(15000);
      // => 15s max on Slow 3G
      // => Worst-case tolerance
    } else if (profileName === "Fast 3G") {
      expect(loadTime).toBeLessThan(8000);
      // => 8s max on Fast 3G
      // => Acceptable mobile experience
    } else if (profileName === "4G") {
      expect(loadTime).toBeLessThan(5000);
      // => 5s max on 4G
      // => Modern mobile baseline
    } else {
      expect(loadTime).toBeLessThan(3000);
      // => 3s max on broadband
      // => Desktop expectation
    }
 
    await browser.close();
    // => Cleans up browser instance
    // => Releases resources
  });
}

Code density: 54 code lines, 103 annotation lines = 1.91 density

Trade-offs and When to Use

Performance API Approach:

  • Use when: Building custom monitoring dashboards, real-user monitoring (RUM), lightweight performance checks
  • Benefits: No external dependencies (300KB+ saved), fast execution (< 100ms), precise control over measurements, works in production via RUM
  • Costs: No Core Web Vitals (LCP/FID/CLS), no actionable insights, manual threshold management, requires deep performance expertise

Lighthouse CI Approach:

  • Use when: CI/CD pipeline gates, comprehensive audits, Core Web Vitals compliance, performance regression detection, team with mixed expertise
  • Benefits: Complete Core Web Vitals coverage, actionable recommendations, automatic threshold enforcement, performance budgets, historical tracking, industry-standard metrics
  • Costs: External dependency (2MB+ with Chrome), slower execution (5-15s per audit), requires Chrome DevTools Protocol setup, can be brittle with dynamic content

Production recommendation: Use Lighthouse CI for CI/CD pipelines and performance regression detection. The comprehensive Core Web Vitals coverage, actionable insights, and automatic threshold enforcement justify the execution time cost (5-15s). The ability to catch performance regressions before production deployment prevents revenue loss from degraded user experience. Use Performance API for real-user monitoring in production to collect actual user performance data and validate Lighthouse lab measurements.

Hybrid approach: Run Lighthouse in CI/CD for pre-deployment validation, instrument Performance API in production for RUM collection, correlate lab vs field data to identify issues Lighthouse can't catch (device-specific problems, geographic latency, third-party performance).

Security Considerations

  • Sensitive Data in Performance Metrics: Resource timing API exposes URLs of all loaded resources, which may leak sensitive information in query parameters or paths. Sanitize URLs before logging or sending to external analytics. Use Timing-Allow-Origin header for cross-origin resources to enable detailed timing.

  • Third-Party Performance Risks: Third-party scripts can degrade performance unpredictably and inject tracking or malicious code. Implement Content Security Policy (CSP) to restrict third-party origins, use Subresource Integrity (SRI) to verify third-party file integrity, monitor third-party resource counts in budgets (max 10), consider self-hosting critical third-party resources.

  • Performance Data Privacy: Performance metrics can fingerprint users and reveal browsing patterns. Aggregate metrics before reporting, avoid logging user-specific performance data with PII, implement GDPR-compliant data retention policies, use differential privacy techniques for public performance dashboards.

  • Denial of Service via Performance Testing: Automated performance tests that run too frequently or hit production systems can inadvertently cause DoS. Rate-limit performance test execution, test against staging environments (not production), implement circuit breakers for test failures, use isolated test accounts with limited data.

  • Credentials in Performance Tests: Hardcoded credentials in performance tests create security vulnerabilities. Use environment variables for credentials, implement secret rotation for test accounts, audit test code for exposed secrets before committing, use separate low-privilege accounts for testing.

Common Pitfalls

  1. Testing Empty Cache Only: Most tests load pages with empty cache, but real users have partial caching. Test both cold (empty cache) and warm (primed cache) scenarios. Use page.route() to simulate cache behavior in tests. Measure cache hit rates in production with Resource Timing API. Implement cache-control headers appropriately for each resource type.

  2. Ignoring Mobile Performance: Desktop tests show good performance, but 60%+ users access via mobile. Test mobile devices with CPU throttling (4x slowdown simulates mid-tier device), test on actual slow networks (not just fast WiFi), validate touch target sizes and mobile-specific interactions, consider bandwidth costs for users on metered connections.

  3. Not Testing Third-Party Impact: Your code performs well, but third-party scripts (analytics, ads, social widgets) destroy performance. Implement performance budgets limiting third-party resource count, measure third-party impact with Resource Timing (initiatorType), use async/defer for non-critical third-parties, implement timeouts for third-party loads (fail gracefully if slow), consider privacy-focused alternatives (self-hosted analytics).

  4. Static Thresholds Across Pages: Homepage gets 2.5s LCP budget, but complex dashboard gets same threshold despite legitimate complexity. Define per-page performance budgets based on functionality (marketing pages: strict, dashboards: lenient), categorize pages by complexity (simple/medium/complex), adjust budgets by route in Lighthouse config, track performance relative to page type (dashboard should be compared to other dashboards, not homepage).

  5. Lighthouse Score Obsession: Teams focus on achieving 100 Lighthouse score instead of actual user experience. Lighthouse score is directional, not absolute truth. Validate lab metrics with real-user monitoring, prioritize Core Web Vitals over overall score (CWV directly impacts search ranking), focus on percentile performance (p75, p95) not just median, measure business metrics (bounce rate, conversion) alongside performance, consider accessibility and best practices scores (not just performance).

  6. Not Tracking Performance Over Time: Running one-off performance tests catches current issues but misses regressions. Implement Lighthouse CI server for historical tracking, store performance metrics in time-series database, create dashboards showing performance trends, set up alerts for performance degradation (>10% regression), correlate performance changes with deployments, track performance in production with RUM (Real User Monitoring).

Last updated February 7, 2026

Command Palette

Search for a command to run...