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-lighthouseLighthouse 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
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.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.
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).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).
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).
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).