lighthouse-perf
Configures Lighthouse CI (`@lhci/cli`) to audit Web Vitals (LCP, INP, CLS) on every PR, asserts against canonical thresholds (LCP ≤2.5s, INP ≤200ms, CLS ≤0.1 at the 75th percentile), uploads Lighthouse reports as build artifacts, and posts deltas as PR comments. Use when the project ships a web frontend and the team needs continuous Web Vitals monitoring tied to PR gating.
lighthouse-perf
Overview
The Core Web Vitals are Google's three canonical user-experience metrics (web-vitals):
| Metric | Measures | "Good" threshold |
|---|---|---|
| LCP (Largest Contentful Paint) | Loading performance | ≤ 2.5 seconds |
| INP (Interaction to Next Paint) | Interactivity | ≤ 200 milliseconds |
| CLS (Cumulative Layout Shift) | Visual stability | ≤ 0.1 |
INP became a stable Core Web Vital in 2024, replacing FID (web-vitals). The canonical measurement standard is the 75th percentile of page loads, segmented across mobile and desktop (web-vitals).
This skill covers Lighthouse CI (@lhci/cli) - the official Google Chrome team tool for running Lighthouse on every PR and asserting against budgets (lhci).
When to use
If the project is a backend API or a CLI tool, this skill doesn't apply - use k6-load-testing or a sibling load runner for backend perf.
Install
npm install --save-dev @lhci/cli(Per lhci; the docs reference @lhci/cli@0.15.x as the current major.) Pin to a specific minor in CI for determinism.
Configure
Create .lighthouserc.js (or .lighthouserc.json) at the project root. The canonical shape per lhci:
module.exports = {
ci: {
collect: {
// What to audit
url: [
'http://localhost:3000/',
'http://localhost:3000/dashboard',
'http://localhost:3000/pricing',
],
// How many runs per URL — median report wins; 3 is canonical for stability
numberOfRuns: 3,
// Lighthouse settings
settings: {
preset: 'desktop', // or 'mobile' (default)
chromeFlags: '--no-sandbox', // CI-runner-friendly
},
// Use a static-server when running headless in CI
startServerCommand: 'npm run start',
startServerReadyPattern: 'ready on',
},
assert: {
// Canonical Web Vitals budgets per web.dev/articles/vitals
assertions: {
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }], // 2.5s
'interaction-to-next-paint': ['error', { maxNumericValue: 200 }], // 200ms
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }], // 0.1
// Lighthouse category scores (0-1)
'categories:performance': ['warn', { minScore: 0.9 }],
'categories:accessibility': ['error', { minScore: 0.95 }],
'categories:best-practices': ['warn', { minScore: 0.9 }],
},
},
upload: {
// Where to upload the .json reports for trend analysis
target: 'temporary-public-storage', // or 'lhci' for self-hosted server
},
},
};Assertion levels per lhci:
Running
The canonical invocation per lhci:
lhci autorunautorun runs three phases in sequence:
For finer control, the phases can run independently: lhci collect, lhci assert, lhci upload.
Running specific URLs
For a single PR-relevant audit:
lhci collect --url=http://localhost:3000/dashboard --numberOfRuns=3
lhci assertCI integration
# .github/workflows/lighthouse.yml
name: lighthouse
on:
pull_request:
paths:
- 'src/**'
- 'package.json'
- 'package-lock.json'
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Build
run: npm run build
- name: Lighthouse CI
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} # optional, for PR comments
run: npx lhci autorun
- name: Upload Lighthouse reports
if: always()
uses: actions/upload-artifact@v4
with:
name: lighthouse-reports
path: .lighthouseci/
retention-days: 14The optional LHCI_GITHUB_APP_TOKEN (set up via the Lighthouse CI GitHub App) enables PR comments showing the per-metric delta vs. the main branch's last green run.
Mobile vs desktop budgets
LCP / INP thresholds are the same across mobile and desktop, but mobile is consistently slower in practice - the same JS bundle runs on a less-powerful CPU over a less-stable network.
Common pattern: separate .lighthouserc.mobile.js and .lighthouserc.desktop.js, run both in CI. The mobile run uses preset: 'mobile' (default) which applies CPU + network throttling to simulate a mid-range Android device.
# Run both
LHCI_BUILD_CONTEXT__GITHUB_BASE_URL=https://github.com/... \
npx lhci autorun --config=.lighthouserc.mobile.js
npx lhci autorun --config=.lighthouserc.desktop.jsAnti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
numberOfRuns: 1 | Single-run measurements are noisy; flaky alerts. | Use 3 (canonical) or 5 for high-stakes pages; LHCI uses the median. |
Asserting on first-input-delay (FID) | FID was retired in 2024 (web-vitals). | Use interaction-to-next-paint (INP). |
| Hard error on every metric out of the box | Existing pages may not meet thresholds; team learns to ignore the gate. | Start with warn for everything; promote to error once green for 2 weeks. |
| Auditing only the homepage | The homepage is usually the most-optimized page; misses regressions on long-tail routes. | Audit a representative URL set: home + 1 logged-in dashboard + 1 long-form content + 1 form-heavy. |
| Lighthouse score as the sole metric | Lighthouse score conflates multiple subscores; doesn't isolate which Web Vital regressed. | Assert on the individual Web Vitals (largest-contentful-paint, interaction-to-next-paint, cumulative-layout-shift); category score is supplementary. |
| Running against production | Lighthouse fires real network requests and triggers analytics; pollutes prod metrics. | Always against staging or a local build. |
Lab vs field
Lighthouse CI measures lab data (synthetic; deterministic runner). Field data (real-user metrics, RUM) is measured by Web Vitals JS in production. Both matter:
| Source | Tool | Use for |
|---|---|---|
| Lab (synthetic) | Lighthouse CI | Per-PR regression gate. |
| Field (RUM) | web-vitals library + analytics | Real-user 75th-percentile tracking. |
Lighthouse CI catches per-PR regressions; field data tracks the 75th-percentile threshold per web-vitals. Don't substitute one for the other.