IAnswerable

Run Lighthouse in CI

01 Apr 2026

Performance regressions don’t get caught by anyone until a user complains, because nobody’s looking at a Lighthouse score until it’s already bad. Run it in CI on every PR instead, with a budget that fails the build, and you catch the regression in the diff that caused it.

Install and configure

npm install -D @lhci/cli
// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: ['http://localhost:3000/', 'http://localhost:3000/pricing'],
      numberOfRuns: 3,
      startServerCommand: 'npm run start',
      startServerReadyPattern: 'ready on',
    },
    assert: {
      assertions: {
        'categories:performance': ['error', { minScore: 0.85 }],
        'categories:accessibility': ['error', { minScore: 0.9 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
      },
    },
    upload: {
      target: 'temporary-public-storage',
    },
  },
}

numberOfRuns: 3 matters — Lighthouse scores vary run to run even on identical code, since they depend on CPU scheduling and timing noise in the environment. A single run gives you a number, not a signal. Three runs and lhci uses the median, which is stable enough to gate a build on.

Wire it into CI

# .github/workflows/lighthouse.yml
jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm run build
      - run: npx lhci autorun

lhci autorun builds, starts the server, runs Lighthouse against each URL in your config, checks the assertions, and exits nonzero if any budget is broken — which fails the CI check and blocks merge, the same as a failing test would.

flowchart TD
    A["PR opened"] --> B["CI: npm run build"]
    B --> C["npx lhci autorun"]
    C --> D{"Assertions pass?"}
    D -->|"yes"| E["Check passes, merge allowed"]
    D -->|"no"| F["Build fails, PR blocked"]
  

Set budgets you’ll actually keep, not aspirational ones

Don’t open with minScore: 1.0. Run Lighthouse against your current production site first, see where you actually are, and set the budget a few points below that — enough room that normal variance doesn’t fail the build, tight enough that a real regression trips it.

# check where you actually stand before setting a budget
npx lighthouse https://your-site.com --view

A budget nobody can hit gets disabled within a month. A budget that’s realistic and slightly strict gets respected, because passing it is achievable and failing it means something actually regressed.

What actually causes the regressions this catches

The gotcha: CI environment noise

CI runners are shared, variable-performance machines, so Lighthouse’s timing-based metrics (LCP, TBT) will be noisier there than on your laptop. That’s expected — set your budgets a bit looser than your local numbers to account for it, and lean on numberOfRuns and the median rather than trying to make CI as stable as a dedicated benchmark machine. If you need tighter, more repeatable numbers for tracking trends over time rather than gating merges, run a separate scheduled job against a real deployed environment instead of PR-time CI.

The whole point is shifting performance review left, from “a user complained” to “the PR that added the regression failed its own check.” That only works if the budget is tight enough to catch a real regression and loose enough that people don’t start ignoring it.