Almost every site is fast on the day it ships. Someone runs a tool, screenshots a good score, and moves on. Then a year of well-meaning changes — a tag here, a font there, one more library to save an afternoon — quietly erodes it, and nobody notices until a customer on a mid-range phone gives up waiting. Performance is not something you achieve once; it is something you defend continuously, or lose.
So the interesting question is not "how fast is the site today" but "what stops it from getting slower next week." Our answer is unglamorous and effective: the budget lives in the build, and the build refuses to merge a change that breaks it. This note is how that works, and what it costs to run.
A budget nobody enforces is just a number
A performance budget is a ceiling — this page may ship no more than this much JavaScript, the largest paint must happen within this much time, layout may shift no more than this much. Written in a document, that ceiling is a good intention that the next deadline will negotiate away. The only budget that holds is one a machine checks on every change, with the authority to say no.
The shift is small to describe and large in effect: stop treating performance as a review someone remembers to do, and start treating a regression as a failing test — the same as a typo that breaks the types or a unit test that goes red. A human can be persuaded that "it is only a few kilobytes." A failing pipeline cannot.
Put the budget where the regression happens
On this site the budgets are part of continuous integration. Every pull request builds the production bundle, measures the first-load JavaScript per route and the Core Web Vitals, and compares them to a fixed ceiling. Exceed it and the check is red; the change does not merge until the weight comes back down or the ceiling is consciously, visibly raised in the same commit. The discipline is that nobody can make the site slower by accident — only on purpose, on the record.
# CI — the build refuses to regress budgets: first-load-js: 180kB # per route, gzipped lcp: 2.5s # mobile, field p75 cls: 0.1 on-exceed: fail # not "warn": a regression blocks the merge
Lab numbers and field numbers are different promises
A score from a tool on a fast laptop is a lab number — useful for catching regressions because it is repeatable, but it is not what your customers feel. Field numbers are what real devices on real networks actually experienced, and they are the ones that matter for both the business and the ranking. The two are not interchangeable, and pretending a good lab score is a good field outcome is one of the quiet dishonesties of our industry.
So we use each for what it is good at: the lab budget in CI catches the regression before it ships, because it is stable and runs on every change; the field data, gathered after launch from actual visitors, is the scoreboard that tells us whether the lab budget was set honestly. When the two disagree, the field wins and the budget gets stricter.
Budgets belong per template, not per site
A single site-wide number hides the truth, because a text-heavy article page and an interactive tool page have genuinely different weights to carry. Average them and you get a ceiling that is too loose for the light pages and too tight for the heavy one, so it ends up protecting neither. The budgets here are set per template — the home, a service page, an article, the audit tool — so each is held to a ceiling that is honest for what it actually does.
This is also where the budget stops a familiar failure: the page that is fast in isolation but slow in aggregate, because every feature added a little and no single addition looked unreasonable. A per-template ceiling makes the aggregate the thing under test, so the tenth small addition is the one that has to justify itself.
What this costs you, honestly
It would be dishonest to present this as free. A build that can block a merge on weight will, sometimes, block a merge on weight — and that means occasionally doing the harder version of a feature instead of the heavy convenient one, or removing something before adding something. That friction is the point; it is the cost of a site that is still fast in three years rather than one that was fast at launch and degraded in silence.
The reward is that performance stops being a periodic crisis — a redesign sprint to claw back what was lost — and becomes a property the system simply maintains. You do not get a fast site by occasionally optimising a slow one. You get it by never letting it become slow in the first place, and that is a decision you make in the pipeline, not the postmortem.
