Tutorial · performance · Published 2026-08-16 · 3 min read
Setting and enforcing performance budgets
Performance budgets: set byte weight, timing and Core Web Vitals limits, then enforce them in Lighthouse CI so performance regressions fail the build.
What to set a budget on
A performance budget is a limit placed on a performance-related quantity or metric so that work stays within an agreed ceiling. Without a budget, performance is a preference; with one, it becomes a constraint checked at build time. Budgets usefully split into three layers that reinforce each other:
- Field metrics: Core Web Vitals at the 75th percentile, measured from CrUX field data. How real users actually experience the site.
- Lab timing and scores: metrics from a controlled Lighthouse or scripting run, such as LCP, Total Blocking Time, and a performance score.
- Quantity budgets on bytes and requests: total page weight, JavaScript kilobytes, image bytes, number of third-party domains.
The field layer tells you whether the outcome is good; the lab and quantity layers are the early-warning signals that catch a regression before it ever reaches the field. The field vs lab article explains why you need both; this article focuses on the budget mechanics.
Choosing default numbers
Industry and vendor guidance gives sensible starting points, but you should derive the authoritative values from your own routes and devices, not adopt them blindly. Common baselines include: JavaScript under roughly 170 KB compressed on the initial load, CSS under roughly 50 KB, total page weight under about 1 MB on mobile for a content site, images under roughly 500 KB of payload per page, and a Core Web Vitals bar of LCP at or under 2.5 seconds, INP at or under 200 ms, and CLS at or under 0.1 at the 75th percentile. In the lab, a common desk target is Total Blocking Time under 200 ms, which correlates with a good INP in the field.
Whatever numbers you pick, budget each important route separately (a checkout page is not a blog page) and tighten toward existing winners rather than global one-size rules. A proven rhythm is the ratchet: when a route comfortably exceeds its target, set the next budget about 10% tighter and re-ship.
Enforcing in Lighthouse CI
Budgets only matter if something stops a regression from landing, and Lighthouse CI makes the assertion easy. The budget.json file in the report's Collect options controls the audible per-resource size budgets (in kilobytes), while the CI assert block handles the per-audit thresholds. Note the unit difference: resource-size budgets use kilobytes, but the numeric assertion equivalents use bytes. A minimal lighthouserc.json looks like this:
{
"ci": {
"collect": {
"url": ["https://example-stage.example/"],
"numberOfRuns": 3
},
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"interaction-to-next-paint": ["error", { "maxNumericValue": 200 }],
"total-byte-weight": ["error", { "maxNumericValue": 1048576 }]
}
}
}
}
Wiring this into the pipeline is described fully in the Lighthouse CI article. Set warnings at roughly 85 to 90% of the ceiling and errors at the ceiling itself, so the team gets early smoke alarms rather than only hard stops. Finally, remember that a lab budget passing says nothing about the field: pair the CI gate with a field-data monitor so a third-party or CDN regression you will not catch in a single synthetic run still surfaces in CrUX over time.