all-posts
Engineering#Performance#CI/CD#Web Vitals

The performance budget every web app needs (and how to enforce it)

Performance is not a launch task — it is a budget line. The exact budgets we set on every client project, the tooling that enforces them in CI, and the exceptions policy that keeps it honest.

Abdenassar Amimi

Founder & Lead Engineer

May 14, 2026

6 min read

Every fast app we have shipped had the same property: it was never allowed to be slow. Not "we optimized it later" — the budget existed before the first component was written, and CI rejected anything that broke it. Performance is a budget line, not a cleanup sprint.

The budgets we set by default

  • →LCP ≤ 2.0s at p75 on a mid-tier Android over 4G. Google’s "good" is 2.5s; we leave margin for the bad days.
  • →Total JS shipped on initial load ≤ 170KB compressed. Frameworks included. It is aggressively doable with server components.
  • →Interaction to Next Paint ≤ 200ms on the core loop actions.
  • →CLS ≤ 0.05 — reserved space for every image and embed, no exceptions.

The JS budget is the one people argue about, so here is the math: 170KB compressed is roughly the entire runtime of a well-built server-component app plus one meaningful client island. Every time a team asks for 500KB, we ask which of their users asked for it. None did. Their product manager’s demo laptop did.

Enforcement lives in CI, or it does not exist

yaml
# .github/workflows/perf.yml (essentials)
- name: Build and measure
  run: |
    pnpm build
    pnpm exec lhci autorun --collect.staticDistDir=.next
  env:
    LHCI_BUDGETS: |

      [
        { "path": "/", "resourceSummary": { "script": { "maxNumericValue": 170000 } } }
      ]
The specifics vary per project, but the shape is always: build, measure, gate.

Lighthouse CI on the PR, budget file in the repo, red build on regression. The red build is the entire system. Dashboards do not change behavior; blocked PRs do. We track field data (real users) separately in a weekly digest — lab data gates the merge, field data catches the things lab data cannot see: your actual users’ devices, networks, and third-party scripts.

The exceptions policy

Rigid budgets get bypassed the first time a legitimate exception appears, so we write the exceptions policy into the budget file itself. An exception needs three things: a named owner, an expiry date, and a linked issue for the paydown. "Temporary" exceptions without expiry dates are how 170 becomes 340 by December.

A performance budget with no exceptions policy is not a budget. It is a New Year’s resolution.

Set the budgets before the first sprint, put the gate in CI before the first PR, and write down the exceptions policy the first time you need one. That is the entire system. Everything else is just enjoying being the fast app in your category.

have-a-project-like-this

This is the kind of thinking we bring to client work. If it resonates, the discovery call is free.

Talk to us