Back to all articles
Frontend6 min read

Core Web Vitals belong in the definition of done, not in a performance sprint

Performance work scheduled for later never happens. Here is the budget system we put in continuous integration on day one, and what it caught before users did.

Written by

Anisha Gurung, Mobile Engineering Lead

Published

February 4, 2026

Every team we meet agrees performance matters. Almost none of them have a mechanism that stops a slow release from shipping.

What they usually have instead is a performance sprint scheduled for after launch. That sprint is the first thing cut when the launch slips, which it does, so the site ships slow and stays slow until someone senior complains about a search ranking or a conversion number.

The fix is not more discipline. It is making the slow release fail the build.

Why later never comes

Performance regressions are individually invisible. Nobody ships a change that adds two seconds. People ship changes that add eighty milliseconds, forty times, over a quarter.

Each of those is defensible on its own. A font here, an analytics tag there, a date library imported for one function, a hero image nobody compressed. No single pull request looks unreasonable in review, and by the time the aggregate is obvious the cause is spread across two hundred commits.

You cannot review your way out of this. Humans are bad at noticing eighty milliseconds and good at rationalising it. You need a number that fails.

The budget we set on day one

Before the first feature, we agree three numbers with the client and put them in the pipeline. These are our defaults for a marketing or content site, measured on a simulated mid range Android over throttled 4G, which is a more honest baseline than a laptop on office wifi.

Metric Budget Google's "good" threshold
Largest Contentful Paint 1.8s 2.5s
Interaction to Next Paint 130ms 200ms
Cumulative Layout Shift 0.05 0.1
Total JavaScript, compressed 140 KB no official figure
Total page weight 800 KB no official figure

We set the budget tighter than the threshold on purpose. If your budget is the threshold, you are permanently one small regression away from failing, and the alert arrives when you are already in trouble. Leaving headroom means the build fails while the fix is still a five minute change.

The two byte budgets are the ones that do the real work. Lab timing metrics are noisy in CI and produce false alarms that teams learn to ignore. Bundle size is deterministic. If a pull request adds 40 KB of JavaScript, that is a fact, and it is nearly always the cause of the timing regression that would have followed.

How it runs in CI

Two checks on every pull request.

Bundle size, on every build. The build output is compared against the base branch. Over budget in total, or a single route growing more than a set threshold, fails the check. The comment on the pull request names the modules that grew.

This catches the most common regression we see, which is a small utility pulling in a large dependency. Someone imports one function from a date library and adds 70 KB because the import was not tree shakeable. In review that line looks completely normal. The check catches it in ninety seconds.

Lighthouse against a real deployment. Every pull request gets a preview deployment, and Lighthouse runs against it three times with the median taken. Three runs matters. A single run in CI has enough variance to produce a failure roughly one time in ten for no reason, and a check that cries wolf gets bypassed within a month.

We fail the build on the byte budgets and warn on the timing metrics, because bytes are deterministic and timings are not. A timing warning that persists across three consecutive merges gets escalated to a failure.

What it caught on a recent build

Real examples from one project over about four months.

A 71 KB icon library for nine icons. The library was imported as a barrel file, so the whole set shipped. Replaced with nine inline SVG components. Saved 68 KB.

A carousel that cost 0.31 CLS. A hero carousel with no reserved height. On a fast connection nobody noticed. On a mid range Android the entire page jumped when it initialised. Caught by the layout shift check on the pull request, fixed with an aspect ratio box in about ten minutes. Had it shipped, it would have been a support ticket at best and a silent conversion loss at worst.

Three separate font loads. Three designers, three components, three font families, each loading four weights. 340 KB of fonts on a page that used two of them. The byte budget failed the third one and the conversation happened in the pull request rather than eight months later.

A tag manager that pulled in 180 KB. Marketing added a tag manager container that loaded four vendor scripts, one of which was a session recorder that ran on every page. The check failed, we moved the non essential scripts behind consent and to an idle callback, and the initial payload stayed inside budget. Marketing kept their tooling. Nobody had to have an argument about priorities, because the constraint was already agreed.

That last one is the underrated benefit. A budget agreed in week one turns a political conversation into a technical one. Nobody is saying no to marketing. The build is saying the page has a weight limit, and the question becomes how to fit inside it.

The framework choices that matter more than the tuning

Most of the performance in a modern site is decided by a handful of architectural choices, not by optimisation afterwards.

Static generation wherever the content allows it. A page rendered at build time and served from a CDN edge starts from a position that no amount of server tuning reaches. Our rule is that a page is dynamic only when it must be, and "must" means it depends on the specific request. Marketing pages, documentation, blog posts and case studies are all static. On this site every route except the form submission endpoint is generated at build time.

Ship less JavaScript, not faster JavaScript. A server component that renders to HTML costs zero kilobytes on the client. We keep client components to the parts that genuinely need interaction: the navigation drawer, the theme toggle, the contact form. Everything else is HTML by the time it reaches the browser.

Self host fonts and preload the one that matters. A font from a third party origin costs a DNS lookup, a connection and a round trip before the first character renders. Self hosted, preloaded, with font-display: swap and a metrics compatible fallback so the swap does not shift layout.

Size every image and never let one be a layout shift. Explicit width and height or an aspect ratio container, modern formats, and a real sizes attribute rather than the default.

Get those four right and you start well inside budget. Then the budget's job is only to stop you drifting out of it.

What it costs to set up

About a day. Half a day for the bundle analysis and the CI comparison, half a day for Lighthouse against preview deployments with the retry logic and threshold tuning.

Against that: the last three performance rescue engagements we were hired for ran between three and seven weeks each, and every one of them was a slow accumulation that a budget in CI would have caught on the individual pull request that caused it.

Performance is not a phase. It is a constraint, and constraints only work when something enforces them.

PerformanceNext.jsSEOFrontend

Working on something similar?

If this article is close to a problem on your desk, we are glad to talk it through. No pitch, just the conversation.

  • A senior engineer reads every brief
  • NDA signed before you share anything sensitive
  • No sales sequence, no automated follow ups