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
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.
[
{
"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.
{
"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.
| Metric | Good target | Budget in CI (suggestion) | Typical regression cause |
|---|---|---|---|
| LCP | under 2.5 s (web.dev) | max. 2.5 s, warn from 2.2 s | New hero image without preload, slower server response |
| INP / TBT | under 200 ms (web.dev) | TBT max. 200 ms | Extra third-party script, an unsplit long task |
| CLS | under 0.1 (web.dev) | max. 0.1 | Ad or banner without reserved space |
| JavaScript weight | keep it lean | max. 400 KB per page | New library, a double-bundled framework |
| Total weight | as small as possible | max. 1 MB per page | Uncompressed 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
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.
- Set a budget per page type – home, category, product and article have different realities
- Put budget.json and the Lighthouse CI configuration into the repository so every change passes through them
- Anchor the check as a required gate in the pull request so a red run blocks the merge
- Take several runs per measurement and use the median to filter out random noise
- On an exceeded budget, open the affected metric in the report and search for the cause in the diff
- Adjust the budget regularly: if a page gets permanently faster, tighten the threshold
The Core in One Sentence
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.
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.
Related Articles
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.
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.
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.