Performance

Lighthouse CI with budgets that block the merge

Lighthouse CI turns LCP, CLS and file weight into assertions that fail the build. Configuration, command and thresholds that stay quiet.

Key points

  • lighthouserc.json declares the assertions and turns a threshold into a test that blocks the merge.
  • numberOfRuns at 3 compares the median instead of a single run.
  • A green CI budget does not guarantee a pass in field data.

Lighthouse CI turns thresholds into a test that blocks the merge: you declare the limits in lighthouserc.json and the build fails when a measurement drops below them. Without that file a budget stays a wish.

Three kinds of threshold

KindExampleEffect
Metriclargest-contentful-paint ≤ 2500error, blocks the merge
Total weighttotal-byte-weight ≤ 500000error on the page budget
Third partiesthird-party-summarywarning, worth a review

The file

{
  "ci": {
    "collect": {
      "url": ["https://ui.eneawork.it/"],
      "numberOfRuns": 3
    },
    "assert": {
      "assertions": {
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "total-byte-weight": ["error", { "maxNumericValue": 500000 }]
      }
    }
  }
}

The numberOfRuns key at 3 makes Lighthouse CI use the median: a single run moves too much to be a reliable threshold.

The command

npx --yes @lhci/cli autorun

The command starts Chrome, runs three passes on the declared URL and compares the results with the assertions. With no upload section it sends nothing to an external server.

Thresholds that stay quiet

  • Gate only stable metrics: LCP and CLS as errors, TBT as a warning.
  • Set the byte budget on the article page, not the home page, because that is the page people read.
  • Measure three times and compare the median: an isolated run is not a regression.

What Lighthouse does not measure

INP does not exist in the lab: Lighthouse measures TBT as a stand-in and leaves the verdict to field data. A green CI budget does not guarantee a pass in the Chrome UX Report.

The limitation

This repository has no lighthouserc file yet: today no job blocks a weight increase. The numbers measured here are here to set the first budget, not to claim a result.

Where to continue

The EneUI catalogue indexes 111,985 components and 1,556 blocks with previews. The hero block is the section that decides the LCP of a landing page: measure it before you pick it. For the thresholds read Core Web Vitals, for the weight of a library the bundle of a UI library, for the measured comparison React libraries 2026.

Frequently asked questions

Does Lighthouse CI really block the merge?

Yes, when the job exits with a non-zero code: GitHub Actions and GitLab read the code and mark the pipeline as failed. You also need a branch protection rule that requires that job.

How many runs do I need?

Three is the practical minimum: Lighthouse CI computes the median and ignores one noisy pass. With a single run the threshold produces false failures.

Can I set thresholds on the scores?

You can, but scores swing a lot between runs and across versions. Thresholds on numeric metrics, such as LCP in milliseconds, are steadier and easier to explain.

Does it work behind a login?

Yes, with a Puppeteer script that fills in the form before the measurement. Apply the budget to the authenticated page, not the sign-in screen.

Budget or assertion?

The budget is the number you declare, the assertion is the rule that enforces it. A budget without an assertion is documentation; an assertion without a budget is an invented threshold.