ESC
Type to search guides, tutorials, and reference documentation.
Verified by Garnet Grid

Web Performance Budgets and Core Web Vitals

How to implement web performance budgets. Covers LCP, FID, CLS optimization, bundle analysis, critical rendering path, and automated performance regression testing.

Web performance isn’t a feature — it’s a constraint. Every 100ms of latency costs 1% of revenue (Amazon). Every second of load time increases bounce rate by 7% (Google). Yet most teams treat performance as an afterthought, optimizing only when users complain. Performance budgets flip this: set hard limits upfront, enforce them in CI/CD, and performance becomes a non-negotiable quality bar.


Core Web Vitals

Google’s Core Web Vitals are the industry standard for measuring user experience:

MetricWhat It MeasuresGoodNeeds WorkPoor
LCP (Largest Contentful Paint)Loading speed≤ 2.5s≤ 4.0s> 4.0s
INP (Interaction to Next Paint)Responsiveness≤ 200ms≤ 500ms> 500ms
CLS (Cumulative Layout Shift)Visual stability≤ 0.1≤ 0.25> 0.25

LCP Optimization

The LCP element is usually a hero image, heading, or video poster. Optimize it with surgical precision:

<!-- Preload the LCP image -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">

<!-- Use responsive images -->
<img src="/hero.webp" 
     srcset="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1200.webp 1200w"
     sizes="(max-width: 768px) 100vw, 50vw"
     loading="eager"
     fetchpriority="high"
     width="1200" height="630"
     alt="Hero image">

LCP Checklist:

  1. Identify the LCP element (Chrome DevTools → Performance → LCP)
  2. Inline critical CSS for the LCP element
  3. Preload the LCP image with fetchpriority="high"
  4. Serve images in WebP/AVIF format
  5. Use a CDN for static assets

CLS Prevention

Layout shifts happen when elements load and push other content around. Every shift degrades user trust.

/* Always set dimensions for images and videos */
img, video {
  width: 100%;
  height: auto;
  aspect-ratio: 16/9;
}

/* Reserve space for dynamic content */
.ad-slot {
  min-height: 250px;
}

/* Prevent font swap flash */
@font-face {
  font-family: 'Custom';
  src: url('/font.woff2') format('woff2');
  font-display: swap; /* or optional for better CLS */
  size-adjust: 100.5%; /* Match fallback font metrics */
}

Performance Budgets

Define hard limits for every page:

MetricBudgetEnforcement
Total JS bundle< 200KB (gzipped)CI/CD gate
Total CSS< 50KB (gzipped)CI/CD gate
LCP< 2.5sLighthouse CI
CLS< 0.1Lighthouse CI
INP< 200msReal User Monitoring
Hero image< 100KBBuild step
Third-party scripts< 3Manual review

Automated Enforcement

# lighthouserc.json
{
  "ci": {
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.90 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "total-byte-weight": ["error", { "maxNumericValue": 500000 }]
      }
    }
  }
}

Run in CI: npx lhci autorun — fails the build if any budget is exceeded.


Bundle Analysis

You can’t optimize what you can’t see. Run bundle analysis on every release:

# Next.js
npx @next/bundle-analyzer

# Webpack
npx webpack-bundle-analyzer dist/stats.json

# Vite
npx rollup-plugin-visualizer

Common bloat sources:

  • Moment.js (330KB) → use date-fns (tree-shakeable) or Temporal API
  • Lodash (full import) → use lodash-es with tree shaking
  • Icon libraries (full set) → import only used icons
  • Unused polyfills → check browser target and remove

Critical Rendering Path

The browser can’t render until it has parsed HTML, downloaded CSS, and executed render-blocking JS. Optimize the critical path:

  1. Inline critical CSS: Extract above-the-fold CSS and inline it in <head>
  2. Defer non-critical CSS: Load below-the-fold styles asynchronously
  3. Async/defer scripts: Never block rendering with JavaScript
<!-- Critical CSS inlined -->
<style>/* above-the-fold styles */</style>

<!-- Non-critical CSS loaded async -->
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

<!-- Scripts with defer -->
<script src="/app.js" defer></script>

Real User Monitoring (RUM)

Lab metrics (Lighthouse) show potential. Field metrics (RUM) show reality. Implement RUM to track actual user experience:

// Web Vitals library
import { onLCP, onINP, onCLS } from 'web-vitals';

function sendToAnalytics(metric) {
  navigator.sendBeacon('/api/vitals', JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    path: window.location.pathname,
    connection: navigator.connection?.effectiveType,
  }));
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);

Track P75 (not average) — this represents the experience of your 75th percentile user, which Google uses for ranking decisions.

Jakub Dimitri Rezayev
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting

Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.

View Full Profile →