Careeroza
Core Web Vitals
The Three Metrics
| Metric | Measures | Good | Common Next.js fix |
|---|---|---|---|
| LCP Largest Contentful Paint | When the main content (hero image/title) appears | ≤ 2.5 s | priority on the hero image, static/ISR rendering, fast TTFB |
| INP Interaction to Next Paint | Delay after a click/tap | ≤ 200 ms | Less client JS, useTransition, debounce |
| CLS Cumulative Layout Shift | Content jumping around | ≤ 0.1 | next/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