Core Web Vitals

The Three Metrics

MetricMeasuresGoodCommon Next.js fix
LCP Largest Contentful PaintWhen the main content (hero image/title) appears≤ 2.5 spriority on the hero image, static/ISR rendering, fast TTFB
INP Interaction to Next PaintDelay after a click/tap≤ 200 msLess client JS, useTransition, debounce
CLS Cumulative Layout ShiftContent jumping around≤ 0.1next/image sizes, next/font, reserved space for ads

Google uses them as a ranking signal and they directly affect conversion: faster shops sell more.

Measuring in Next.js

// app/web-vitals.tsx
'use client';

import { useReportWebVitals } from 'next/web-vitals';

export function WebVitals() {
  useReportWebVitals((metric) => {
    navigator.sendBeacon('/api/vitals', JSON.stringify({ name: metric.name, value: metric.value, page: location.pathname }));
  });
  return null;
}

Example

Classify collected values with Google's thresholds (the 75th percentile of real users is what counts):

const thresholds = { LCP: [2500, 4000], INP: [200, 500], CLS: [0.1, 0.25] } as const;
type Metric = keyof typeof thresholds;

function rate(name: Metric, value: number): string {
  const [good, poor] = thresholds[name];
  return value <= good ? 'good' : value <= poor ? 'needs improvement' : 'poor';
}

function p75(values: number[]): number {
  const s = [...values].sort((a, b) => a - b);
  return s[Math.ceil(0.75 * s.length) - 1];
}

const lcp = [1800, 2100, 2300, 2600, 3900, 2200, 2000, 5200];
const inp = [80, 120, 150, 260, 90, 110, 140, 600];
const cls = [0.01, 0.0, 0.05, 0.12, 0.02, 0.0, 0.3, 0.04];
console.log('LCP p75', p75(lcp), 'ms ->', rate('LCP', p75(lcp)));
console.log('INP p75', p75(inp), 'ms ->', rate('INP', p75(inp)));
console.log('CLS p75', p75(cls), '    ->', rate('CLS', p75(cls)));

Output

LCP p75 2600 ms -> needs improvement
INP p75 150 ms -> good
CLS p75 0.05     -> good