Frontend performance directly governs user conversion rates, engagement depth, and search indexing rank (via Google's Core Web Vitals ranking signals). Yet performance tuning frequently remains reactive: teams discover latency regressions post-factum after code has shipped to production.
Manual browser audits suffer from high variance: browser extensions, dynamic caching, background operating system processes, and workstation hardware disparities skew measurements. Establishing reliable performance baselines demands automating synthetic performance audits directly inside the continuous integration pipeline (CI/CD).
Lighthouse CI (@lhci/cli) provides an industry-standard testing infrastructure:
Lighthouse CI orchestrates the complete audit lifecycle from localized production builds to trace metric collection and assertion enforcement.
+-------------------------------------------------------------------------+
| GitHub Actions Runner (Ubuntu Container) |
+-------------------------------------------------------------------------+
|
v 1. Build & Collect (lhci collect)
+-------------------------------------------------------------------------+
| Next.js 15 / React Production Build Server |
| pnpm build && pnpm start (Port 3000) |
+-------------------------------------------------------------------------+
|
v CDP (Chrome DevTools Protocol)
+-------------------------------------------------------------------------+
| Headless Chrome with Throttling Profile |
| - CPU Slowdown: 4x (Simulates mid-tier mobile processor) |
| - Network Throttling: Fast 4G (RTT 150ms, Throughput 1.6 Mbps) |
| - Viewport: Mobile (360x640) or Desktop (1366x768) |
| - Number of Runs: 3 (Computes representative Median Run) |
+-------------------------------------------------------------------------+
|
v 2. Raw Trace & Performance Metrics
+-------------------------------------------------------------------------+
| Core Web Vitals Extractor |
| |
| LCP (Largest Contentful Paint) <= 2.5s -> [Pass / Fail] |
| CLS (Cumulative Layout Shift) <= 0.10 -> [Pass / Fail] |
| INP (Interaction to Next Paint) <= 200ms -> [Pass / Fail] |
| TBT (Total Blocking Time) <= 200ms -> [Pass / Fail] |
+-------------------------------------------------------------------------+
|
v 3. Assertions Engine (lhci assert)
+-------------------------------------------------------------------------+
| Resource Budget Validation (budget.json) |
| |
| Total JS Bundle Size <= 180 KB -> [Pass / Fail] |
| Third-party Scripts <= 50 KB -> [Pass / Fail] |
+-------------------------------------------------------------------------+
|
+------------------+------------------+
| |
[PASS] [FAIL]
v v
PR Check: Green PR Check: Red / Block Merge
Metrics within SLA bounds LCP regressed by 400ms (> 2.5s)fetchpriority="high".min-height, aspect-ratio) for asynchronously inserted advertising blocks or cookie notices.lighthouserc.js#The configuration defines the three core pipeline phases: collect (data gathering), assert (threshold validation), and upload (report artifact storage):
module.exports = {
ci: {
collect: {
// Boot production server prior to launching browser audits
startServerCommand: 'pnpm run start',
startServerReadyPattern: 'ready on http://localhost:3000',
startServerReadyTimeout: 60000,
url: [
'http://localhost:3000/',
'http://localhost:3000/catalog',
'http://localhost:3000/pricing',
],
// 3 runs to calculate a stable median run and filter CI runner variance
numberOfRuns: 3,
settings: {
// Desktop or mobile throttling emulation profile
preset: 'desktop',
throttlingMethod: 'simulate',
// Skip audits irrelevant to local synthetic environments
skipAudits: ['uses-http2', 'is-on-https'],
},
},
assert: {
// Use official recommended base ruleset
preset: 'lighthouse:recommended',
assertions: {
// 1. Core Web Vitals: Strict production thresholds
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'total-blocking-time': ['error', { maxNumericValue: 200 }],
// 2. High-level category scores (0.0 to 1.0)
'categories:performance': ['error', { minScore: 0.90 }],
'categories:accessibility': ['error', { minScore: 0.95 }],
'categories:best-practices': ['error', { minScore: 0.95 }],
'categories:seo': ['error', { minScore: 0.95 }],
// 3. Performance Budgets
'resource-summary:script:size': ['error', { maxNumericValue: 180000 }], // 180 KB
'resource-summary:total:size': ['error', { maxNumericValue: 600000 }], // 600 KB
'dom-size': ['error', { maxNumericValue: 1200 }],
},
},
upload: {
// Store HTML reports in temporary public storage or self-hosted LHCI server
target: 'temporary-public-storage',
},
},
};budget.json#Fine-grained resource budgeting restricts network payloads across asset classifications and third-party integrations:
[
{
"path": "/*",
"timings": [
{
"metric": "interactive",
"budget": 3000
},
{
"metric": "first-contentful-paint",
"budget": 1200
}
],
"resourceSizes": [
{
"resourceType": "script",
"budget": 180
},
{
"resourceType": "stylesheet",
"budget": 45
},
{
"resourceType": "image",
"budget": 250
},
{
"resourceType": "font",
"budget": 80
},
{
"resourceType": "third-party",
"budget": 50
},
{
"resourceType": "total",
"budget": 600
}
],
"resourceCounts": [
{
"resourceType": "third-party",
"budget": 5
},
{
"resourceType": "script",
"budget": 10
}
]
}
]INP captures the responsiveness of real user actions. Components use standard browser PerformanceObserver APIs to track Long Tasks and event timing latency:
/**
* Initializes Core Web Vitals monitoring and user interaction latency profiling
*/
export function registerPerformanceObservers(): void {
if (typeof window === 'undefined' || !('PerformanceObserver' in window)) {
return;
}
try {
// 1. Monitor Largest Contentful Paint (LCP)
const lcpObserver = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
if (lastEntry) {
console.info(`[Web Vitals] LCP: ${lastEntry.startTime.toFixed(2)}ms`, lastEntry);
}
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
// 2. Track Cumulative Layout Shift (CLS)
let clsValue = 0;
const clsObserver = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
const layoutShift = entry as PerformanceEntry & { hadRecentInput?: boolean; value: number };
// Exclude layout shifts caused by direct recent user interactions
if (!layoutShift.hadRecentInput) {
clsValue += layoutShift.value;
console.info(`[Web Vitals] Cumulative CLS: ${clsValue.toFixed(4)}`);
}
}
});
clsObserver.observe({ type: 'layout-shift', buffered: true });
// 3. Monitor Interaction to Next Paint (INP) via Event Timing API
const inpObserver = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
const eventEntry = entry as PerformanceEntry & {
duration: number;
processingStart: number;
processingEnd: number;
interactionId?: number;
};
// Log interactions exceeding the critical 200ms threshold
if (eventEntry.duration > 200) {
console.warn(
`[Web Vitals Slow INP] Event "${eventEntry.name}" took ${eventEntry.duration.toFixed(2)}ms`,
{
inputDelay: eventEntry.processingStart - eventEntry.startTime,
processingTime: eventEntry.processingEnd - eventEntry.processingStart,
presentationDelay: eventEntry.duration - (eventEntry.processingEnd - eventEntry.startTime),
}
);
}
}
});
inpObserver.observe({ type: 'event', buffered: true, durationThreshold: 40 });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Unknown performance observer error';
console.error(`Failed to register PerformanceObserver: ${message}`);
}
}performance-gate.yml#This workflow compiles an optimized production build, runs @lhci/cli, and reports status directly to GitHub pull request checks:
name: Performance Quality Gate (Lighthouse CI)
on:
pull_request:
branches: [main]
jobs:
lhci:
name: Lighthouse CI Web Vitals Audit
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v3
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Next.js production bundle
run: pnpm run build
- name: Run Lighthouse CI
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
run: |
pnpm exec lhci autorun
- name: Post LHCI status comment
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = './.lighthouseci/manifest.json';
if (fs.existsSync(path)) {
const manifest = JSON.parse(fs.readFileSync(path, 'utf8'));
console.log('Lighthouse CI runs completed:', manifest.length);
}Auditing development servers completely invalidates metrics: unminified JavaScript bundles, disabled tree-shaking, massive sourcemaps, and React Dev Mode overhead degrade LCP and TBT by 3x–5x, causing guaranteed false test failures.
# ❌ WRONG: Running lhci against development server
startServerCommand: "pnpm run dev"
# ✅ CORRECT: Audit strictly against optimized production builds
startServerCommand: "pnpm run start" # Preceded by pnpm run buildCloud CI runners (GitHub Actions, GitLab CI) execute on shared multi-tenant hosts. A single audit run (numberOfRuns: 1) is highly vulnerable to CPU throttling spikes on the host machine, causing false-positive threshold failures.
Solution: Always configure numberOfRuns: 3 or 5. LHCI automatically identifies and evaluates the median run.
First Input Delay (FID) measured only the input delay of the initial interaction on a page, ignoring event handler processing time and subsequent frame rendering. INP evaluates all user interactions across the entire session. Optimizing solely for initial input leaves subsequent interactions sluggish and unresponsive.
Define target audit URLs, emulation profiles, and production server boot commands.
Enforce byte caps for JavaScript chunks, stylesheets, web fonts, and third-party scripts.
Set hard thresholds: LCP <= 2.5s, CLS <= 0.1, Total Blocking Time <= 200ms in assert block.
Configure 3 to 5 audit iterations to filter virtualization noise in shared CI infrastructure.
Integrate lhci autorun into PR workflows to block merging code that breaches performance SLAs.