Performance Budgets
Date: 2026-08-16
A threshold that fails a build. Anything softer is a dashboard nobody reads — performance decays by default, one reasonable addition at a time, and only an automatic gate reverses that.
What it is
A performance budget is a quantified limit on some property of a page, enforced automatically, where exceeding it blocks a change from shipping.
The enforcement is the definition. A target that produces a warning is a target; a target that fails CI is a budget.
Why decay is the default
No single change makes a site slow. Each addition is individually reasonable — a review widget, a personalisation script, a slightly larger hero, another font weight — and each is approved by someone who isn’t looking at the cumulative number.
month 1 LCP 1.9s ✓
month 4 LCP 2.2s ✓ "still passing"
month 8 LCP 2.6s ✗ and nobody can say which change did it
In plain terms: performance is a commons. Everyone’s contribution is small, nobody’s is decisive, and without a gate the total only goes one way.
What to budget
Three kinds, and you want at least one from each.
| Kind | Examples | Best for |
|---|---|---|
| Quantity | JS bytes, image weight, request count, third-party origins | Easy to measure, deterministic, catches regressions early |
| Timing | LCP, INP, CLS, TTFB, Total Blocking Time | Closest to experience, noisier in CI |
| Rule | No render-blocking scripts; no unfingerprinted assets; no new third-party origin without review | Binary, unambiguous, easy to enforce |
Quantity budgets are the most practical to gate on because they’re stable run to run. Timing budgets in a lab are noisy — use a median of several runs and a generous threshold, or gate on quantity and monitor timing in the field.
Setting the numbers
Not aspirationally. Three defensible sources:
- Your current p75, minus a little. Prevents regression, which is most of the value
- The Core Web Vitals thresholds — LCP 2.5s, INP 200ms, CLS 0.1 at p75 — as the outer bound
- Faster than your competitors on the same templates, if the argument needs an external anchor
A starting set for a retail site:
JS (initial, compressed) < 170 KB
images (above the fold) < 300 KB
fonts (total) < 60 KB
third-party origins < 10
requests before first paint < 20
LCP (lab, throttled 4× CPU) < 2.5s
Total Blocking Time < 300ms
CLS < 0.05
Budget per template, not site-wide. A product page and a category page have different content and different budgets, and one site-wide number either strangles the heavy template or lets the light one drift.
Enforcing it
1 CI runs a lab measurement on key templates, fixed config, CPU throttled
2 compare against the budget file, committed to the repo
3 exceed it → the build fails, with the offending metric named
4 raising the budget requires a pull request someone reviews
That last point is what makes it work. The budget will need raising sometimes — a genuinely valuable feature costs bytes — and the point isn’t to prevent that. It’s to make the cost visible and the decision explicit rather than absorbed silently.
Lighthouse CI, bundle-size checkers and most CDNs’ CI integrations all support this; the mechanism matters less than the failure being blocking.
Pairing it with the field
Lab budgets catch regressions before they ship. They don’t tell you whether real users improved.
- Gate in CI on lab and quantity budgets — deterministic, fast, blocking
- Monitor in the field on Core Web Vitals — real devices, real connections, and the number that actually counts — Real User Monitoring, Field vs Lab Data
- Alert on field regression with a longer window, because field data is noisy and lagged — Alerting on Metrics
Failure modes
- Warnings instead of failures. Ignored within a fortnight
- One site-wide budget across templates with different content
- Set so loosely nothing ever fails — a budget that has never blocked anything isn’t a budget
- Set so tightly everything fails, so the team routes around it or disables it
- No route to raise it. People bypass a gate they can’t legitimately open
- Third parties exempted, which exempts the largest contributor — Third-Party Scripts, Tag Manager Performance
- Nobody owns it. A failing build with no owner gets skipped. See Performance Culture