Loading introduction
Loading stats
Loading about
Loading posts
Loading article
Skip to content
Md Nayeem Hossain
Explorer
home.tsx
web-performance-budgets-core-web-vitals.md
web-performance-budgets-core-web-vitals.md
blog.json

// Frontend

Web Performance Budgets and Core Web Vitals in Practice

Fix LCP, INP and CLS for real users, then keep them fixed with a performance budget that fails the build when a regression slips in.

Md Nayeem HossainJul 21, 20253 min read
⚛️

Web Performance Budgets and Core Web Vitals in Practice#

Performance work has a frustrating pattern. A team spends a sprint making the site fast, scores improve, everyone celebrates — and six months later the site is slow again. Nobody made it slow on purpose. It got slow one small dependency and one unoptimized image at a time.

The fix is not another optimization sprint. It is a performance budget enforced in CI, built around the metrics that actually reflect user experience.

The metrics that matter#

Google's Core Web Vitals are a practical, user-centered set of measurements:

  • Largest Contentful Paint (LCP) — how long until the main content is visible. Target: under 2.5 seconds.
  • Interaction to Next Paint (INP) — how quickly the page responds to clicks, taps and key presses. Target: under 200 milliseconds.
  • Cumulative Layout Shift (CLS) — how much the content jumps around while loading. Target: under 0.1.

Measure them at the 75th percentile of real users, not on your fast laptop on office Wi-Fi.

Lab data vs field data#

Lighthouse in CI is lab data: consistent, repeatable, good for catching regressions. Real user monitoring is field data: messy but true, collected from actual devices and networks.

You need both. Lab tests stop regressions before they ship. Field data tells you whether users are actually having a good experience. A page can score 100 in Lighthouse and still feel slow to users on mid-range Android phones.

Collecting field data is a few lines with the web-vitals package:

ts
1import { onCLS, onINP, onLCP } from "web-vitals";2 3function send(metric: { name: string; value: number; id: string }) {4  navigator.sendBeacon("/api/vitals", JSON.stringify(metric));5}6 7onLCP(send);8onINP(send);9onCLS(send);

Fixing LCP#

LCP is usually a hero image or a large heading. The common causes and fixes:

  • The LCP image loads late. Do not lazy-load it. Mark it high priority so the browser fetches it immediately.
  • Images are too large. Serve modern formats (AVIF, WebP) at the size actually displayed, using responsive srcset.
  • Render-blocking resources. Inline critical CSS, defer non-critical scripts, and preconnect to required origins.
  • Slow server response. Cache HTML at the edge where possible. No front-end optimization fixes a two-second TTFB.
tsx
1<Image src="/hero.avif" alt="" width={1200} height={630} priority sizes="100vw" />

Fixing INP#

INP measures how busy the main thread is when the user interacts. It gets worse as JavaScript grows.

  • Ship less JavaScript. Audit bundles, remove unused dependencies, and use server rendering for content that does not need interactivity.
  • Break up long tasks. Any task over 50 ms blocks input. Yield to the browser between chunks of heavy work.
  • Defer non-urgent updates. In React, useTransition marks expensive re-renders as interruptible so typing stays responsive.
  • Watch third-party scripts. Chat widgets, analytics and tag managers are frequent INP offenders. Load them late, or not at all on critical pages.

Fixing CLS#

Layout shift is almost always caused by content without reserved space:

  • Always set width and height (or aspect-ratio) on images and video.
  • Reserve space for ads, embeds and banners before they load.
  • Use font-display: swap with size-adjusted fallback fonts to avoid text reflow.
  • Never insert content above existing content unless it is a response to user action.

Set a budget and enforce it#

A budget turns performance from an aspiration into a test that can fail. Define limits for the things that cause regressions:

json
1{2  "budgets": [3    { "resourceType": "script", "budget": 170 },4    { "resourceType": "image", "budget": 300 },5    { "metric": "largest-contentful-paint", "budget": 2500 },6    { "metric": "cumulative-layout-shift", "budget": 0.1 }7  ]8}

Run it in CI on every pull request. When a change exceeds the budget, the build fails, and the conversation happens before the regression ships: is this dependency worth 40 KB?

Budgets work best with an owner. Someone reviews the trend monthly and adjusts limits deliberately rather than raising them whenever a build fails.

Make performance visible#

Put a field-data dashboard where the team sees it: p75 LCP, INP and CLS per key page, over time, broken down by device type. When performance is visible, people notice regressions themselves.

Conclusion#

Lasting performance comes from measuring what users feel, fixing the specific causes of each vital, and enforcing a budget so regressions fail in CI instead of in production. Optimization sprints make a site fast once. Budgets keep it fast.

#Performance#Core Web Vitals#Lighthouse#Next.js
OlderDesigning a Multi-Tenant SaaS ArchitectureNewer JWT, Sessions and Refresh Tokens: Getting Authentication Right

Outline

  • Web Performance Budgets and Core Web Vitals in Practice
  • The metrics that matter
  • Lab data vs field data
  • Fixing LCP
  • Fixing INP
  • Fixing CLS
  • Set a budget and enforce it
  • Make performance visible
  • Conclusion