Why good intentions fail
Performance is everyone's priority in principle and nobody's task in practice. It degrades gradually — an analytics script here, a date library there — and no single change is ever worth blocking a release for.
By the time it is bad enough to notice, fixing it is a project rather than a pull request.
Make it a build failure
The only performance policy we have seen hold is one enforced automatically:
- A byte budget per route, checked in CI. Exceed it and the build fails.
- Lighthouse CI on the key templates, with thresholds for LCP, CLS and INP.
- A dependency size check on pull requests, so the cost of adding a library is visible at review time rather than discovered in production.
Numbers we start from: 130KB of JavaScript for a marketing page, 250KB for an application route, LCP under 2.0s, CLS under 0.05, INP under 200ms.
The four changes that do most of the work
Server-render everything above the fold. The hero should not wait for hydration. This alone usually moves LCP more than every other optimisation combined.
Give images explicit dimensions. Layout shift is almost entirely images and web fonts arriving without reserved space.
Self-host fonts with size-adjust. A fallback font metrically matched to the real one removes
the reflow when it swaps in.
Be deliberate about client components. In an App Router codebase, "use client" is the main way
bundles grow. Every one should be a decision someone could defend in review.
Raising a budget is allowed
A budget is not a moral position. If a route genuinely needs more, raise it — in a pull request, with a sentence explaining why.
The point was never the number. It was making the cost visible at the moment someone chooses to pay it.