Performance budgets only work when they fail the build
Every team we meet has a performance target. Almost none of them have a mechanism that stops a regression from shipping. Those are very different things.
A performance budget that lives in a slide deck is a wish. A performance budget that lives in your CI pipeline and turns a pull request red is a constraint. Only the second kind changes behaviour.
Why targets drift
Nobody ships a 400KB regression deliberately. It arrives as a chart library imported for one dashboard, a date utility pulled in whole, an analytics tag added by marketing with the best of intentions. Each addition is individually defensible. The aggregate is a site that got 40% heavier over two quarters and nobody can say exactly when.
Reviewers cannot catch this, because the diff never looks alarming. Machines can.
What we actually enforce
- Route-level JavaScript budgets. Each route gets a byte ceiling. Exceeding it fails the build with the specific import that caused it.
- Lighthouse CI on the three routes that matter. Home, category, checkout. Not every page — just the ones with a revenue argument attached.
- Interaction latency in synthetic tests. INP is easy to regress and invisible in lab metrics unless you deliberately test it.
- Real-user monitoring as the tiebreaker. Lab numbers tell you what changed; field numbers tell you whether it mattered.
The uncomfortable part
The first month after turning this on is unpleasant. Builds fail for reasons people find annoying, and somebody will ask for an exemption within the first week. Grant it, once, with an expiry date on it. What you are actually installing is the idea that page weight has an owner, and that owner is whoever wrote the pull request.
Six months later the arguments stop, because the budget has become part of how the team thinks rather than something imposed on it.
