Every team has run a Lighthouse audit, celebrated a 96, and watched it drift to 71 over the following quarter. The audit was not wrong. It was just a photograph of a moving thing.
Performance is not a score you achieve. It is a constraint you enforce, and the only place a constraint holds is in the pipeline.
Budget the bytes, not the score
Lighthouse scores are noisy — they move with machine load, network conditions and the phase of the moon. Failing a build on a composite score produces flaky CI and, within two weeks, a team that skips the check.
Budget the things that are deterministic:
{
"budgets": [
{
"path": "/*",
"resourceSizes": [
{ "resourceType": "script", "budget": 180 },
{ "resourceType": "stylesheet", "budget": 60 },
{ "resourceType": "font", "budget": 120 },
{ "resourceType": "total", "budget": 500 }
]
}
]
}
Bundle size is stable across runs. If the JavaScript budget goes from 178KB to 214KB, something specific happened in this pull request, and the diff is right there.
Keep the Lighthouse run, but report it rather than gate on it. Gate on bytes.
Attribute the regression in the PR
A build that fails with "bundle too large" sends someone on a twenty-minute hunt. A build that fails with "+34KB — date-fns added to the client bundle" is fixed in two minutes.
Emit a comparison against the base branch as a PR comment: per-chunk deltas, and the new dependencies responsible. The point is to make the cheapest moment to fix a regression also the most obvious one.
The four things that actually regress
Across the front-ends we have built, budget failures cluster:
A date or utility library reaching the client. Almost always importable server-side only, or replaceable with Intl. Intl.DateTimeFormat covers the majority of what teams reach for date-fns to do.
An icon set imported wholesale. import * as Icons pulls in thousands. Bundle the icons you use explicitly — and remember that dynamically named icons (:name="item.icon") are invisible to static scanners, so they need listing by hand.
Fonts. Self-host, subset to the ranges you serve, preload only the weights used above the fold. Four weights of two families at full Latin coverage is 300KB nobody asked for.
Images without dimensions. Not a byte problem, a layout-shift problem, and the one most likely to make a fast site feel slow. Width and height on every image, always.
Measure the field, not the lab
Lab metrics tell you whether a change helped. Field metrics tell you whether your users are having a good time. They disagree more often than people expect, because your laptop is not a mid-range Android on a train.
Ship web-vitals, send p75 to your analytics, and put LCP, INP and CLS on a dashboard someone looks at weekly. When lab and field diverge, the field is right.
Reasonable starting numbers
For a content-led marketing site, these are achievable without heroics:
| Metric | Target (p75, field) |
|---|---|
| LCP | < 1.5s |
| INP | < 150ms |
| CLS | < 0.05 |
| JS transferred | < 180KB |
| Total transferred | < 500KB |
An application will need looser numbers. Set them deliberately rather than inheriting them, and write down why.
The part that is cultural
The technical setup takes an afternoon. Keeping it takes agreement that a budget failure blocks a merge like a failing test does.
Without that, the check becomes advisory, advisory becomes ignored, and you are back to running an audit every quarter and wondering where the score went.
Related reading
Your component API is a contract, so write it down
Variant tables, prop naming and the boolean that should have been an enum. Notes from maintaining component libraries other people have to use.
Design tokens that survive a rebrand
Most token systems break the first time the brand changes. The fix is a layer most teams skip — and it costs about a day to add.
Designing for models that are wrong
Every AI feature has a failure rate. Most interfaces are designed as though it were zero, and users learn to distrust the whole product.
One useful email a month
Design system patterns, front-end techniques and case study breakdowns. No promotions, no digest of other people's links.


