Skip to main content
Ganesh Joshi
Back to Blogs

Web Vitals: measure and improve Core Web Vitals

February 15, 20265 min read
Tutorials
Performance or metrics on screen

Core Web Vitals measure real user experience: how fast content appears, how quickly the page responds to interaction, and how stable the layout is. Google uses these metrics as ranking signals. More importantly, good scores mean better user experience.

The three Core Web Vitals

Metric Measures Good Needs Improvement Poor
LCP Loading performance ≤2.5s 2.5-4s >4s
INP Interactivity ≤200ms 200-500ms >500ms
CLS Visual stability ≤0.1 0.1-0.25 >0.25

Largest Contentful Paint (LCP)

LCP measures when the largest visible content element (image, video, text block) finishes rendering. It approximates when users perceive the page as "loaded."

Common LCP candidates:

  • Hero images
  • Large text blocks
  • Video poster images
  • Background images

Interaction to Next Paint (INP)

INP measures responsiveness by observing all click, tap, and keyboard interactions throughout the page's lifecycle. It reports the worst interaction (with outliers excluded).

Unlike FID (which measured only the first interaction), INP captures ongoing responsiveness.

Cumulative Layout Shift (CLS)

CLS measures visual stability—how much content shifts unexpectedly. Each time visible elements move after rendering, it adds to the CLS score.

Common causes:

  • Images without dimensions
  • Ads and embeds without reserved space
  • Dynamically injected content
  • Web fonts causing text reflow

Measuring Web Vitals

Lab data (controlled environment)

Tool Best for
Lighthouse Auditing and debugging
Chrome DevTools Performance Detailed analysis
PageSpeed Insights Quick scores
WebPageTest Advanced testing

Field data (real users)

Source Access
Chrome UX Report (CrUX) PageSpeed Insights, BigQuery
web-vitals library Your analytics
Google Search Console Your site's data
Vercel Analytics Next.js projects

Field data reflects real user experience across different devices and networks.

Using the web-vitals library

npm install web-vitals
import { onLCP, onINP, onCLS } from 'web-vitals';

function sendToAnalytics(metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    id: metric.id,
    delta: metric.delta,
  });

  navigator.sendBeacon('/api/analytics', body);
}

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

Next.js Web Vitals

// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <SpeedInsights />
      </body>
    </html>
  );
}

Or use the custom reporter:

// pages/_app.tsx
export function reportWebVitals(metric) {
  console.log(metric);
  // Send to analytics
}

Improving LCP

Identify the LCP element

  1. Open Chrome DevTools
  2. Run Lighthouse or Performance audit
  3. Look for "Largest Contentful Paint element"

Optimization strategies

Issue Fix
Slow server response Use CDN, optimize backend
Render-blocking resources Defer non-critical JS/CSS
Large images Compress, use modern formats
Client-side rendering Use SSR or SSG
Slow font loading Preload critical fonts

Prioritize LCP image

<!-- HTML -->
<img src="hero.jpg" fetchpriority="high" />

<!-- Next.js -->
<Image src="/hero.jpg" priority />

Preload critical resources

<link rel="preload" href="/hero.jpg" as="image" />
<link rel="preload" href="/font.woff2" as="font" type="font/woff2" crossorigin />

Improving INP

Identify slow interactions

Use Chrome DevTools Performance panel:

  1. Record interactions
  2. Look for long tasks (>50ms)
  3. Identify blocking JavaScript

Optimization strategies

Issue Fix
Long tasks Break into smaller chunks
Heavy JavaScript Code split, lazy load
Layout thrashing Batch DOM reads/writes
Third-party scripts Load async, defer

Yield to main thread

// Break up long work
async function processItems(items) {
  for (const item of items) {
    process(item);

    // Yield every 50ms
    if (performance.now() - start > 50) {
      await new Promise(resolve => setTimeout(resolve, 0));
      start = performance.now();
    }
  }
}

Use web workers for heavy computation

// worker.ts
self.onmessage = (e) => {
  const result = heavyComputation(e.data);
  self.postMessage(result);
};

// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url));
worker.postMessage(data);
worker.onmessage = (e) => handleResult(e.data);

Improving CLS

Set explicit dimensions

<!-- Always include width and height -->
<img src="photo.jpg" width="800" height="600" alt="..." />

<!-- Or use aspect-ratio -->
<img src="photo.jpg" style="aspect-ratio: 4/3; width: 100%;" alt="..." />

Reserve space for dynamic content

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

/* Skeleton for loading content */
.skeleton {
  height: 200px;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
}

Handle web fonts

/* Prevent layout shift from font loading */
@font-face {
  font-family: 'CustomFont';
  src: url('/font.woff2') format('woff2');
  font-display: swap;
}

/* Or optional for non-critical fonts */
@font-face {
  font-family: 'DecorativeFont';
  font-display: optional;
}

Avoid inserting content above viewport

// Bad: shifts content down
container.prepend(newElement);

// Better: insert below or use animation
container.append(newElement);

Debugging specific issues

Find layout shifts

// Log all layout shifts
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('Layout shift:', entry);
    console.log('Sources:', entry.sources);
  }
}).observe({ type: 'layout-shift', buffered: true });

Find long tasks

// Log long tasks
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('Long task:', entry.duration, 'ms');
  }
}).observe({ type: 'longtask' });

Setting up monitoring

Custom dashboard

// Aggregate and track over time
function reportVitals(metric) {
  fetch('/api/vitals', {
    method: 'POST',
    body: JSON.stringify({
      metric: metric.name,
      value: metric.value,
      page: window.location.pathname,
      connection: navigator.connection?.effectiveType,
    }),
  });
}

Alerting thresholds

Metric Alert threshold
LCP p75 > 2.5s
INP p75 > 200ms
CLS p75 > 0.1

Alert on 75th percentile to catch issues affecting most users.

Summary

Core Web Vitals measure loading (LCP), interactivity (INP), and stability (CLS). Measure with lab tools for debugging and field data for real user experience. Improve LCP with image optimization and preloading. Improve INP by breaking up long tasks and deferring heavy JavaScript. Improve CLS with explicit dimensions and reserved space. Monitor continuously to catch regressions.

Frequently Asked Questions

Core Web Vitals are Google's metrics for user experience: Largest Contentful Paint (LCP) for loading, Interaction to Next Paint (INP) for interactivity, and Cumulative Layout Shift (CLS) for visual stability.

LCP should be under 2.5 seconds for good user experience. Between 2.5s and 4s needs improvement. Over 4s is poor. Measure with PageSpeed Insights or Chrome DevTools.

Interaction to Next Paint (INP) replaced FID in March 2024. INP measures responsiveness across all interactions, not just the first one. Good INP is under 200ms.

Set explicit width and height on images and embeds. Reserve space for dynamic content. Avoid inserting content above existing content. Use font-display: swap with preloaded fonts.

Yes. Core Web Vitals are a Google ranking factor. Good scores won't override content quality, but they can be a tiebreaker between similar pages and improve user engagement.

Related Posts