Engineering Practice May 20, 2026 7 min read

Performance Budgets: How We Hold Production Sites Under 2.5 Seconds

Admin
Admin Lead Writer & Contributor
“It should be fast” is not a budget. We have watched this play out on a lot of projects. Kickoff includes a slide that says “performance is critical.” Three sprints later the page renders in 5 seconds on a mid-range Android, and nobody is sure where the time went. The marketing team adds three new tracking scripts. The CMS pumps out 4 MB hero images. The product manager asks for an animated carousel above the fold. Each decision in isolation is small. Together they ship a slow site. The fix is a budget. A specific, written-down, measurable budget that exists at kickoff and gets checked on every deploy. Not “fast.” Not “great Lighthouse score.” A list of numbers a feature has to fit inside, or it does not ship.

Why “Fast Enough” Is Not a Budget

“Fast enough” is a feeling. Budgets are numbers. The difference matters because feelings shift over time. The first version of the site loads in 1.8 seconds. Everyone agrees that is fast. Six months later the same site loads in 4.2 seconds. Each new feature added 100 ms. No single sprint introduced the slow site. The drift did. A budget catches the drift on the deploy that introduces it. If the LCP target is 2.5 seconds and a PR pushes it to 2.7, the PR does not ship. The conversation happens that day, not three months later when the slow site is the new normal.
Aspirational performance is a marketing line. Budgeted performance is engineering practice.

The Four Numbers We Publish Before Kickoff

These numbers are aligned with Google’s “Good” Core Web Vitals thresholds — what real users feel and what Google ranks against. We hold all four at the 75th percentile of real users.

Core Performance Budget

  • LCP ≤ 2.5 seconds — Largest Contentful Paint, p75 real users
  • INP ≤ 200 ms — Interaction to Next Paint, p75 real users
  • CLS ≤ 0.10 — Cumulative Layout Shift, p75 real users
  • Total transfer ≤ 1.5 MB on first load (JS bundle ≤ 200 KB gzip)
Four numbers. They go on the kickoff doc. The client signs off. The engineering team owns hitting them.

What We Measure, and What We Ignore

We measure three places: lab data for the developer feedback loop, field data for the truth, and CI gates for the contract.

1. Lab Data — Lighthouse in CI

Every PR runs Lighthouse against a preview build. We do not gate on the overall score because Lighthouse score noise is real. We gate on the specific Core Web Vitals metrics with hard thresholds.
# .github/workflows/lighthouse.yml (excerpt)
- name: Run Lighthouse CI
  run: |
    npx lhci autorun \
      --collect.url=https://preview-${{ github.event.number }}.example.com \
      --collect.numberOfRuns=3 \
      --assert.preset=lighthouse:no-pwa \
      --assert.assertions.largest-contentful-paint='["error", {"maxNumericValue": 2500}]' \
      --assert.assertions.cumulative-layout-shift='["error", {"maxNumericValue": 0.10}]' \
      --assert.assertions.total-blocking-time='["error", {"maxNumericValue": 200}]'
Three runs, median value, error if over budget. Total Blocking Time is the lab proxy for INP because lab cannot simulate real user interactions.

2. Field Data — web-vitals Library

Lab data tells you what your build does on a controlled machine. Field data tells you what your users see on their machines.
// src/lib/vitals.js
import { onLCP, onINP, onCLS } from 'web-vitals/attribution';

const send = (metric) => {
    const body = JSON.stringify({
        name: metric.name,
        value: metric.value,
        rating: metric.rating,
        navigationType: metric.navigationType,
        attribution: metric.attribution,
        page: location.pathname,
    });
    navigator.sendBeacon('/api/vitals', body);
};

onLCP(send);
onINP(send);
onCLS(send);
Beacon to an endpoint. Endpoint pushes to a time-series store. Dashboard shows p75 per page over the last 7 days. Alert fires when any metric exceeds budget for three consecutive deploys. The attribution import is the underused one — when LCP regresses, attribution tells you which element took the longest.

3. Chrome User Experience Report

Google publishes p75 LCP, INP, CLS, and FCP for any origin with enough Chrome traffic. The CrUX dashboard at g.co/chromeuxdash is free. We pull it into our own monitoring once a month for sites that have CrUX coverage.

What We Cut When the Budget Breaks

A budget that never bends is fiction. A budget that bends without rules is theater. Here are our three rules, in order.

What We Measure

  • Core Web Vitals metrics directly
  • Field data at p75 per page
  • Lab data on every PR
  • CrUX monthly for real-origin trends
  • LCP element via attribution API

What We Ignore

  • Lighthouse score as a single number
  • Synthetic tests from a single location
  • “Eyeball it on my laptop” testing
  • Cloudflare auto-optimization proxies
  • Optimize-later roadmap items

Three Patterns That Quietly Blow the Budget

The dramatic budget killers are obvious — the 4 MB hero image, the 600 KB bundle, the auto-play carousel. Those get caught in code review. The quiet ones are harder.

1. Third-Party Tag Bloat

The marketing team adds Google Analytics. Then Hotjar. Then a chat widget. Then a heatmap tool. Then a “trust seal” script. Then an A/B testing harness. Each script is small in isolation. Together they are a kilobyte concert that runs on every page load. The fix is a third-party budget: a maximum of three third-party scripts on production. Each new request goes through review — what does this script do, and is the answer worth the LCP cost? A recent client wanted six third-party scripts that added 740 ms to LCP on mobile. They kept two. LCP came back under budget.

2. Web Fonts Without font-display: swap

A self-hosted font file is 80 KB. The browser delays text rendering until the font loads. LCP suffers because the hero headline cannot paint. The fix is one CSS line in every @font-face block.
@font-face {
    font-family: 'Onest';
    src: url('/fonts/onest-variable.woff2') format('woff2-variations');
    font-weight: 100 900;
    font-display: swap;
}
swap tells the browser to render the fallback font immediately and swap when the real font loads. LCP fires on the fallback render. If the font is critical to brand identity, pair font-display: swap with <link rel="preload" as="font"> so the swap happens sooner.

3. JavaScript Hydration on Static Content

A page that is 90 percent static content is rendered with React, served as JSON, and hydrated in the browser. The user waits for 200 KB of JavaScript to download, parse, and execute before the page is interactive. The fix depends on the framework. Next.js: use static export (output: 'export') for content pages that do not need runtime React. Astro: islands architecture by default. Plain HTML: ship plain HTML. We have rebuilt several “React marketing sites” as Astro and watched LCP drop by a full second with zero visible change to the user.

Budgets as Scope Discipline

The budget is not just a performance tool. It is a scope tool. When a new feature request comes in, the question is no longer “can we build this?” The question is “can we build this inside the budget?” If the answer is no, the feature either gets a smaller version or the budget gets a written exception with sign-off from the team. The conversation happens with the client at kickoff, not after launch when the slow site is shipped. Clients who do not want this conversation tend not to want us. Clients who do tend to stay for years.

Need a Build Held to a Real Performance Budget?

TechMaven runs this practice from kickoff — four numbers, CI gates, field-data monitoring, and a monthly review that keeps drift from becoming a fire. Start a Project

The Bottom Line

Performance is not a final-week polish phase. It is a kickoff conversation, a written budget, a CI gate, a field-data dashboard, and a monthly review. Each piece is small. The total cost is roughly one engineer-day per month for a typical production site. The four numbers: LCP ≤ 2.5 s, INP ≤ 200 ms, CLS ≤ 0.10, total transfer ≤ 1.5 MB. p75, real users. Write them down before kickoff. If you need a build held to a real performance budget from kickoff, the engineering team behind TechMaven runs this practice. Start a project.