Skip to content
Journal

Engineering

Performance Budgets That Actually Stick

Headshot of Grace Nakamura
Grace Nakamura
April 5, 2023 · 3 min read

Nobody argues against web performance. Bring it up in any meeting and everyone nods: faster is better, Core Web Vitals affect rankings, milliseconds cost conversions. Then the same site gains a chat widget, a second analytics tag, a hero video, and a marketing team's tag manager full of mystery scripts, and eighteen months later someone commissions a "performance audit" as if the cause were unknowable. Performance does not decay because people stop caring. It decays because caring is not a mechanism.

The mechanism is a budget: hard numeric limits, checked by a machine, that make performance a shared constraint instead of a shared sentiment. Here is how we run them on client work.

Pick few numbers, tie them to users

A budget with fifteen metrics is a dashboard, not a budget. We set limits on a handful of things that map to felt experience:

  • Largest Contentful Paint under 2.5 seconds at the 75th percentile on real devices, because that is the threshold users, and Google's Core Web Vitals assessment, actually experience.
  • Cumulative Layout Shift under 0.1, because layout jank reads as brokenness even when nothing is broken.
  • A JavaScript budget in kilobytes per route. For the content site we shipped this quarter: 170KB compressed for the critical path. JS is the most expensive byte type you can send; it is parsed, compiled, and executed on a phone that is not the one on your desk.
  • Interaction responsiveness, which we are watching closely via INP, the newer responsiveness metric Chrome has been developing. Even while the industry standard remains FID, FID is too easy to pass; it flatters slow sites.

The budget is agreed at kickoff, with the client, in the statement of work. That last part is the trick: it converts "the engineers want it faster" into "we all signed this."

Make the machine say no

A budget nobody enforces is a wish. Ours run in CI on every pull request, using Lighthouse CI against built preview deployments:

{
  "assertions": {
    "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
    "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
    "resource-summary:script:size": ["error", { "maxNumericValue": 174080 }]
  }
}

When a change would blow the budget, the build fails, and the conversation happens at the pull request, when it costs minutes, rather than at the quarterly review, when it costs a project. Crucially, failing is allowed: sometimes the new feature is worth the bytes. But the trade is now explicit. Someone must either find an offsetting saving or consciously raise the budget in a reviewed commit. Slow happens on purpose or not at all.

The third-party tag problem is a governance problem

The hardest budget conversations are not about our code. They are about the marketing stack. Every tag is somebody's KPI, and no single tag is the problem, which is exactly how a site ends up with 900KB of other people's JavaScript. Our approach: third parties get their own budget line, every tag has a named owner and a review date, and anything unclaimed at review time is removed. On one recent engagement, simply asking "who owns this tag?" retired a quarter of them; the owners had left the company.

Lab numbers lie a little, so watch the field

CI budgets run in a lab, and the lab is optimistic. We pair them with real-user monitoring and check the field data monthly, because your actual audience owns mid-range Android phones on hotel Wi-Fi. The lab catches regressions before they ship; the field tells you the truth about what shipped already. You need both, and the field data settles arguments the lab cannot.

Performance is a feature with a cost model, not a virtue. Give it a number, an owner, and a build that fails, and it stops being the thing everyone agrees about while the site gets slower.

Building something this could apply to?

We take on a small number of flagship projects each quarter.

Start a project