Testland
Browse all skills & agents

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):

MetricMeasures"Good" threshold
LCP (Largest Contentful Paint)Loading performance2.5 seconds
INP (Interaction to Next Paint)Interactivity200 milliseconds
CLS (Cumulative Layout Shift)Visual stability0.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

  • The project ships a web frontend (Next.js, Vite, Remix, plain static, etc.).
  • The team has documented Web Vitals NFRs (per nfr-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 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:

  • '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 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:

lhci collect --url=http://localhost:3000/dashboard --numberOfRuns=3
lhci assert

CI 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: 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.

# 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-patternWhy it failsFix
numberOfRuns: 1Single-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 boxExisting 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 homepageThe 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 metricLighthouse 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 productionLighthouse 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:

SourceToolUse for
Lab (synthetic)Lighthouse CIPer-PR regression gate.
Field (RUM)web-vitals library + analyticsReal-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.

References

  • web-vitals - canonical Core Web Vitals definitions and thresholds (LCP ≤2.5s, INP ≤200ms, CLS ≤0.1; 75th-percentile measurement; INP-replaces-FID in 2024).
  • lhci - Lighthouse CI canonical install, lhci autorun, configuration shape, assertion levels.
  • lighthouse-budget-author - sibling skill for drafting budgets at design time (this skill is the runner).
  • perf-budget-gate - downstream unified gate aggregating Lighthouse + load-runner verdicts.
  • nfr-extractor - upstream skill that surfaces Web Vitals NFRs from PRDs.