Skip to content
Fundamentals & strategy

Performance Budget in CI: Stop Regressions Automatically

A performance budget as a budget.json rule checks every pull request in Lighthouse CI and stops the build the moment LCP, INP or CLS breach the threshold.

11 min read Performance-BudgetLighthouse CICI/CDCore Web VitalsRegression

A website rarely turns slow in one go. It does so piece by piece: a new carousel on the home page, an extra tracking script, a hero image at full resolution, a library someone pulls in for a small detail. Each single change looks harmless, yet in sum the hard-won Core Web Vitals slip out of the green again – usually unnoticed, because after the deploy nobody measures. This is exactly where a performance budget comes in: it sets fixed upper limits for load time, responsiveness, layout stability and page weight in advance, and lets a machine check on every change whether those limits are kept. As a budget.json rule in Lighthouse CI, this check moves right into the development process and stops the build automatically the moment a value breaches the threshold. That the effort pays off is shown by one figure from practice alone: only 62 percent (Web Almanac 2025) of mobile websites even reach a good LCP under 2.5 seconds. This article shows how a budget is built, how Lighthouse CI turns it into a gatekeeper in the pull request, and how to calibrate the thresholds so they are strict but not flaky. In our Core Web Vitals optimization, such a budget is the building block that secures the result for good.

Key takeaways

  • A performance budget sets upper limits in advance – for outcome metrics such as LCP, INP and CLS and for quantity budgets such as kilobytes of JavaScript or number of requests. The metrics protect the experience, the quantity limits protect the metrics.
  • Good means an LCP under 2.5 seconds (web.dev), an INP under 200 milliseconds (web.dev) and a CLS under 0.1 (web.dev) at the 75th percentile – yet only 62 percent (Web Almanac 2025) of mobile websites reach a good LCP.
  • The rule file budget.json describes timing and resource limits per path and sits versioned in the repository. Only Lighthouse CI turns it into a gate: if a measurement breaches a threshold marked as an error, the run ends with an error code.
  • Thresholds need a deliberate buffer and the median of several runs. A gate that turns red on mere measurement noise loses the team's trust and gets waved through – then it is worthless even though it technically works.
  • The budget measures in the lab and does not replace field data: INP can only be approximated there via Total Blocking Time, and CLS is captured only until the page has finished building. Only field measurement shows what real users experience.

Why Every Deploy Quietly Degrades Your Core Web Vitals

Performance is not a property you establish once and that then stays. It is a state that is at stake again with every code change. The tricky thing about performance regressions is that they happen gradually and produce no visible error: the page still works, it just loads a bit slower, responds a bit more sluggishly, or briefly shifts the layout while building up. For the person who checked in the change, everything looks fine on a fast office machine with a stable network. Only the real users on mid-range smartphones on mobile networks feel the degradation – and they show up in no bug report; they simply leave.

The economic consequences are well documented. Just 0.1 seconds (Deloitte) of faster load time raised retail conversions by 8.4 percent (Deloitte) in a cross-industry analysis. Conversely, the probability that a user leaves the page again rises by 32 percent (Google/SOASTA) when load time grows from one to three seconds. And the conversion rate drops, according to an analysis of millions of page views, by about 4.4 percent (Portent) with every additional second in the first five seconds. Every unnoticed regression therefore costs measurable money. The classic reflex remedy – a big performance audit every few months – comes too late for these small, continuous degradations: by the next audit, dozens of mini-regressions have accumulated, and nobody knows anymore which change contributed what.

A performance budget flips the logic. Instead of hunting for degradations after the fact, it prevents them at the source. It makes performance a condition every change must meet before it may enter the main branch – the way automated tests ensure a change does not break an existing function. Anyone who wants to understand the three Core Web Vitals in context will find the classification in the article on Core Web Vitals 2026; here it is about how to enforce their compliance permanently.

What a Performance Budget Is and What It Measures

A performance budget is a collection of upper limits a page must not exceed. These limits concern two kinds of values. First, the outcome metrics, that is, what the user experiences: Largest Contentful Paint (LCP) for load time, Interaction to Next Paint (INP) for responsiveness and Cumulative Layout Shift (CLS) for visual stability. Considered good are an LCP under 2.5 seconds (web.dev), an INP under 200 milliseconds (web.dev) and a CLS under 0.1 (web.dev), each measured at the 75th percentile (web.dev) of page loads. Second, the quantity budgets that limit causes: how many kilobytes of JavaScript, images or fonts a page may load and how many requests it fires.

The distinction matters. Outcome metrics say whether it is too slow for the user; quantity budgets say why. A JavaScript budget of, say, 400 kilobytes per page often catches a regression earlier than a metric, because additional code as a rule increases weight first and response time only afterwards. Both budget types therefore complement each other: the metrics protect the experience, the quantity limits protect the metrics. How to set up and maintain a pure resource budget for scripts and media is explored in the article on JavaScript performance budgets.

Lab and Field Metrics in the Budget

In CI, a budget first checks lab data from a controlled measurement. INP cannot be captured directly in the lab because it needs real interactions – so Lighthouse uses Total Blocking Time (TBT) as a lab proxy. A low TBT is a strong signal for a good INP in the field. CLS, in turn, is captured in the lab only up to the end of page load, but in the field across the whole session. A CI budget therefore does not replace field measurement; it catches regressions before they even reach the user.

budget.json: the Rule File for Resources and Metrics

The heart of it is a plain text file called budget.json. In a clearly readable format it describes which upper limits apply – separated into time metrics (timings) and resource quantities (resourceSizes and resourceCounts). Each rule refers to a path, so different page types can be treated differently. The file lives in the repository next to the code and is thus versioned itself: every change to the budget is traceable, and the budget goes through the same reviews as the rest of the project.

budget.json
[
  {
    "path": "/*",
    "timings": [
      { "metric": "largest-contentful-paint", "budget": 2500 },
      { "metric": "total-blocking-time", "budget": 200 },
      { "metric": "speed-index", "budget": 3400 }
    ],
    "resourceSizes": [
      { "resourceType": "script", "budget": 400 },
      { "resourceType": "image", "budget": 500 },
      { "resourceType": "total", "budget": 1000 }
    ]
  }
]

In this example, LCP may be at most 2,500 milliseconds, Total Blocking Time at most 200 milliseconds, and the delivered JavaScript at most 400 kilobytes. Lighthouse reads this file, compares the measured values with the budgets and flags every item above the limit as over budget. That alone, however, does not yet stop a build – it only produces a report. To turn the report into a real gate, a second building block is needed: Lighthouse CI.

Lighthouse CI as the Gatekeeper in the Pull Request

Lighthouse CI (the open-source toolchain GoogleChrome/lighthouse-ci) is the bridge between a Lighthouse report and your pipeline. It consists of three steps: collect gathers the measurements by running Lighthouse several times against the defined addresses; assert compares the results with set thresholds; and upload stores the reports, optionally on a dedicated LHCI server for the long-term history. Decisive for the gate is the assert step: if a measurement violates a threshold marked as error, the tool exits with a non-zero error code. In any CI environment – whether GitHub Actions, GitLab CI or another pipeline – such an error code means: the run is red, the check fails, the merge is blocked.

lighthouserc.json
{
  "ci": {
    "collect": {
      "url": ["https://example.com/"],
      "numberOfRuns": 3
    },
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "total-blocking-time": ["error", { "maxNumericValue": 200 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }]
      }
    }
  }
}

This configuration requires a performance score of at least 0.9 and sets hard limits for LCP, TBT and CLS. You can combine budget.json and the assertions: one covers the resource quantities, the other the outcome metrics and the overall score. It is important that the check does not run on the developer's machine but in the pipeline – reproducibly, with throttled network and CPU, as Lighthouse assumes by default for the mobile measurement. Only then does the lab result roughly match what users experience on weaker devices.

One Gate per Pull Request

Lighthouse CI runs automatically on every pull request, before the code even reaches the main branch. If a budget is exceeded, the check turns red and the merge cannot be completed until the cause is fixed.

Median of Several Runs

Instead of a single, noisy measurement, Lighthouse CI takes the median of several runs. This dampens random outliers and makes the threshold reliable enough to enforce hard.

History, Not a Snapshot

The optional LHCI server stores every run and shows the trend over time. This makes it visible whether a metric slowly drifts away from the target over many small steps, long before it breaches the budget.

Calibrate the Thresholds Right: Strict but Not Flaky

A budget is only as useful as its thresholds. If they are too loose, regressions slip through and the gate lulls you into false security. If they are too strict or too close to the current measurement, the build fails on normal measurement noise – and a team that constantly sees red runs without a real reason gets used to ignoring them. A good budget therefore starts at the official target values and leaves a small, deliberate buffer. The median of several runs and a warning level below the hard error limit help separate the noise from genuine degradations.

MetricGood targetBudget in CI (suggestion)Typical regression cause
LCPunder 2.5 s (web.dev)max. 2.5 s, warn from 2.2 sNew hero image without preload, slower server response
INP / TBTunder 200 ms (web.dev)TBT max. 200 msExtra third-party script, an unsplit long task
CLSunder 0.1 (web.dev)max. 0.1Ad or banner without reserved space
JavaScript weightkeep it leanmax. 400 KB per pageNew library, a double-bundled framework
Total weightas small as possiblemax. 1 MB per pageUncompressed media, many third-party requests

It is also important to differentiate the budget per page type. The home page, a category page, a product page and a blog article have completely different realities: what is generous for a text-heavy article page would be unrealistically tight for an image-heavy product page. That is why budget.json allows different rules per path. A frequent trigger for exceeded budgets are third-party scripts that creep in unnoticed via tag managers; how to reduce that load deliberately is covered in the article on trimming third-party scripts.

An Overly Flaky Gate Is Worse Than None

When a build turns red from harmless measurement noise, the team quickly loses trust in the check and starts waving red runs through wholesale. That makes the gate worthless even though it works technically. Set the hard error limit at some distance from the current measurement, use the median of at least three runs and separate warnings from errors. The goal is a gate that only fires on genuine degradations – then every red run is taken seriously.

From Red Build to Green Pipeline: the Workflow

In practice, the budget fits into the entirely normal development flow. Someone opens a pull request, the pipeline starts, builds the page, runs Lighthouse CI against the relevant addresses and compares the results with the budget. If all values stay within range, the check turns green and the change can be merged. If a budget is exceeded, the check turns red, and the report shows exactly which metric or which resource lies above the limit. The responsible person sees this before the merge and can search for the cause in the manageable diff of the pull request, rather than later in the grown overall system.

  1. Set a budget per page type – home, category, product and article have different realities
  2. Put budget.json and the Lighthouse CI configuration into the repository so every change passes through them
  3. Anchor the check as a required gate in the pull request so a red run blocks the merge
  4. Take several runs per measurement and use the median to filter out random noise
  5. On an exceeded budget, open the affected metric in the report and search for the cause in the diff
  6. Adjust the budget regularly: if a page gets permanently faster, tighten the threshold

The Core in One Sentence

A performance budget shifts quality control from good intentions into the pipeline: it is not the human who has to remember to measure after every deploy, but the build that stops on its own the moment a metric breaches the budget.

The side effect is as culturally valuable as it is technical. Once the gate is in place, performance becomes the shared responsibility of the whole team rather than the task of a single person who tidies up every few months. Because the feedback comes immediately in the pull request, every developer learns at once which patterns are expensive – a late-loaded hero image, an unsplit long task, a heavy library. How to deliberately split such long tasks with scheduler.yield and track down the brakes in individual frames is shown in two in-depth articles on interactivity.

What a Budget Cannot Replace: the Field Data

As effective as a CI budget is – it measures in the lab, not with real users. The lab delivers reproducible values under controlled conditions and is therefore ideal for detecting regressions between two states. What real people experience on their devices and networks, however, only field data shows. And the difference is economically relevant: if load time grows from one to five seconds, the bounce probability rises by 90 percent (Google/SOASTA), and in the travel segment, just 0.1 seconds of faster loading brought 10.1 percent (Deloitte) more conversions. These effects arise in the field, not in the lab.

That is why the lab budget and field measurement belong together: the budget in CI prevents a regression from ever going live; the field measurement confirms that the values hold with real users too. How lab and field data – that is, Lighthouse runs, RUM and Google's CrUX data – sensibly combine into an overall strategy is explored in the article on RUM, CrUX and performance budgets. And because INP can only be approximated in the lab via TBT, a complementary look at INP optimization in detail is worthwhile.

A budget in CI changes the culture in the team: performance is no longer a one-off action that fizzles out after a few months, but a condition every pull request must meet – checked automatically before anyone forgets it.

Project experience from 50+ performance projects

This is exactly how we proceed when setting it up: we identify the critical page types, calibrate realistic budgets for LCP, INP and CLS, anchor Lighthouse CI as a required check in your pipeline and support the team until red runs are understood as a helpful signal rather than a nuisance. Which services this includes is shown in our overview of performance services; for the concrete frontend levers with which an exceeded budget can be reeled back in, our frontend optimization helps.

This article is based on data from: web.dev (Core Web Vitals, Google Chrome), Lighthouse and Lighthouse CI (GoogleChrome/lighthouse-ci), the Web Almanac 2025 (HTTP Archive), Deloitte (Milliseconds Make Millions), Google/SOASTA (Mobile Page Speed Benchmarks) and Portent (Site Speed and Conversion). All statistics cited were verified at the time of publication.

Related Articles

Fundamentals & strategy

Writing Speed Into the Contract: Performance in Tenders

How load time becomes a verifiable acceptance criterion: target value, method, data source and proof date per metric – and what German contract law makes of it.

14 min read
Front-end optimisation

View Transitions API: Seamless Page Navigation in Shops

Native, GPU-accelerated page changes in your shop without a JavaScript framework: how cross-document view transitions smooth the jump from category to product.

12 min read
Core Web Vitals & measurement

Break Up Long Tasks: Lower INP with scheduler.yield

Break long JavaScript tasks over 50 ms into chunks with scheduler.yield(), free the main thread between the chunks and keep the INP steadily under 200 ms.

12 min read