Skip to content
Back to Quality Assurance

Core Web Vitals

Track Google's three user-centric metrics: LCP, INP, and CLS, and keep them in the 'good' range.

Why

Core Web Vitals are the metrics that Google uses in its search ranking algorithm and that directly correlate with user engagement and conversion. LCP (Largest Contentful Paint) measures when the main content appears — over 2.5s and users perceive the page as slow. INP (Interaction to Next Paint) measures responsiveness — over 200ms and interactions feel laggy. CLS (Cumulative Layout Shift) measures visual stability — over 0.1 and buttons jump as the user tries to click, causing mis-clicks and frustration. These are not abstract numbers; they are the difference between a 2% and a 4% conversion rate, and Google penalizes slow pages in search results. Tracking them is tracking whether your page is fast enough for users to stay.

When

Track on every page in production (RUM — Real User Monitoring). Set budgets in CI (Lighthouse CI) and block deploys that regress LCP/INP/CLS beyond threshold.

How

1. Measure in production with RUM (Real User Monitoring)
   - Use web-vitals library to collect LCP, INP, CLS from real users
   - Send to analytics (Google Analytics 4, Datadog RUM, Vercel Analytics)
   - Look at the 75th percentile, not the average — averages hide the slow tail

2. Measure in CI with Lighthouse
   - npx @lhci/cli autorun — runs Lighthouse on every PR
   - Set budgets: LCP < 2.5s, INP < 200ms, CLS < 0.1
   - Fail the CI build if a budget is regressed (not if it is already over — only regressions)

3. Optimize LCP (Largest Contentful Paint)
   - The LCP element is usually a hero image or a large text block — find it
   - Preload the LCP image: <link rel="preload" as="image" href="hero.jpg">
   - Serve images in next-gen formats (WebP, AVIF) with responsive sizes
   - Remove render-blocking scripts and CSS; inline critical CSS

4. Optimize INP (Interaction to Next Paint)
   - Long tasks (>50ms) block interaction — find them with Performance Observer
   - Break long JavaScript into smaller chunks; use requestIdleCallback for non-urgent work
   - Debounce input handlers; do not do heavy work synchronously on click

5. Optimize CLS (Cumulative Layout Shift)
   - Always set width and height on images and videos — reserves space, no jump
   - Do not inject ads or banners above existing content — reserve their slot
   - Use CSS transform for animations, not properties that trigger layout (top, left)
   - Load web fonts with font-display: optional or preload to prevent FOIT/FOUT

Example

// Collect Core Web Vitals from real users
import { onLCP, onINP, onCLS, onFCP, onTTFB } from "web-vitals";

function sendToAnalytics(metric) {
  fetch("/api/vitals", {
    method: "POST",
    body: JSON.stringify({
      name: metric.name,
      value: metric.value,
      id: metric.id,
      rating: metric.rating, // "good" | "needs-improvement" | "poor"
      page: location.pathname,
    }),
  });
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);

// Lighthouse CI budget: lighthouserc.json
{
  "ci": {
    "assert": {
      "assertions": {
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "interaction-to-next-paint": ["warn", { "maxNumericValue": 200 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "first-contentful-paint": ["warn", { "maxNumericValue": 1800 }]
      }
    }
  }
}

Common Problems

  • ⚠Measuring only in CI with Lighthouse (synthetic) and not in production with RUM — CI runs on a fast machine with a clean cache, so it reports good scores while real users on slow phones with 3G see 5-second LCP.
  • ⚠Optimizing the average instead of the 75th percentile — the average is 1.5s but 25% of users see 4s, and those are the users who bounce; Google ranks on p75, not average.
  • ⚠Setting a Lighthouse budget but not failing the CI build — the budget is a warning that everyone ignores, and performance regresses every sprint because there is no enforcement.
  • ⚠Fixing CLS by adding a min-height to one element while other images still load without dimensions — the overall CLS improves slightly but the page still jumps when those images load.

Best Practices

  • ✓Measure in production with RUM (web-vitals library → analytics) and look at the 75th percentile per page — this is what Google sees and what real users experience.
  • ✓Set Lighthouse CI budgets as errors (not warnings) and fail the build on regression — a warning is ignored, an error forces the developer to fix the regression before merge.
  • ✓Reserve space for every image, ad slot, and embed with explicit width/height or aspect-ratio — this eliminates the most common source of CLS with zero runtime cost.
  • ✓Preload the LCP element (hero image or font) with <link rel="preload"> so the browser starts fetching it before it discovers it in the HTML — this is the single highest-impact LCP optimization.