git:20260706.d9cec5f to git:20260718.ab29907

2 added, 3 removed. Audit A to A.

---
name: lighthouse-perf
description: "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][webvitals]):
[webvitals]: https://web.dev/articles/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][webvitals]). The canonical measurement standard is
**the 75th percentile** of page loads, segmented across mobile and
desktop ([web-vitals][webvitals]).
[lhci]: https://github.com/GoogleChrome/lighthouse-ci
This skill covers **Lighthouse CI** (`@lhci/cli`) - the official
Google Chrome team tool for running Lighthouse on every PR and
asserting against budgets ([lhci][lhci]).
## When to use
- The project ships a web frontend (Next.js, Vite, Remix, plain
static, etc.).
- - The team has documented Web Vitals NFRs (per
- [`nfr-extractor`](../../../qa-shift-left/skills/nfr-extractor/SKILL.md))
+ - The team has documented Web Vitals NFRs (per `non-functional-requirement-extractor`)
and needs CI enforcement.
- A PR's perf delta vs. main needs to be visible (regression-block
or just-warn).
- The team uses Lighthouse for accessibility / SEO / best-practices
audits beyond perf.
If the project is a backend API or a CLI tool, this skill doesn't
apply - use [`k6-load-testing`](../k6-load-testing/SKILL.md) or a
sibling load runner for backend perf.
## Install
```bash
npm install --save-dev @lhci/cli
```
(Per [lhci][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][lhci]:
```js
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][lhci]:
- `'error'` - fails the CI run (exits non-zero).
- `'warn'` - surfaces in the report but doesn't fail.
- `'off'` - disabled (useful for opt-out per-page).
## Running
The canonical invocation per [lhci][lhci]:
```bash
lhci autorun
```
`autorun` runs three phases in sequence:
1. **collect** - start the server (if `startServerCommand` set), run
Lighthouse N times per URL.
2. **assert** - compare results against the assertion config; exit
non-zero on `error`-level failures.
3. **upload** - upload reports to the configured target for trend
tracking.
For finer control, the phases can run independently: `lhci collect`,
`lhci assert`, `lhci upload`.
### Running specific URLs
For a single PR-relevant audit:
```bash
lhci collect --url=http://localhost:3000/dashboard --numberOfRuns=3
lhci assert
```
## CI integration
```yaml
# .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: 14
```
The 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.
```bash
# Run both
LHCI_BUILD_CONTEXT__GITHUB_BASE_URL=https://github.com/... \
npx lhci autorun --config=.lighthouserc.mobile.js
npx lhci autorun --config=.lighthouserc.desktop.js
```
## Anti-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][webvitals]). | 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][webvitals]. Don't substitute
one for the other.
## References
- [web-vitals][webvitals] - canonical Core Web Vitals definitions and
thresholds (LCP ≤2.5s, INP ≤200ms, CLS ≤0.1; 75th-percentile
measurement; INP-replaces-FID in 2024).
- [lhci][lhci] - Lighthouse CI canonical install, `lhci autorun`,
configuration shape, assertion levels.
- [`lighthouse-budget-author`](../lighthouse-budget-author/SKILL.md) -
sibling skill for **drafting** budgets at design time (this skill
is the **runner**).
- [`perf-budget-gate`](../perf-budget-gate/SKILL.md) - downstream
unified gate aggregating Lighthouse + load-runner verdicts.
- - [`nfr-extractor`](../../../qa-shift-left/skills/nfr-extractor/SKILL.md) - upstream skill that surfaces Web Vitals NFRs from PRDs.
+ - `non-functional-requirement-extractor` - upstream skill that surfaces Web Vitals NFRs from PRDs.