useReportWebVitals

useReportWebVitals and Field Data

Lab numbers tell you whether a change helped; only field data tells you what users get. useReportWebVitals from next/web-vitals wraps the same library and fires your callback for TTFB, FCP, LCP, FID, CLS and INP as each becomes available. It needs 'use client', so give it its own component, and keep the callback reference stable: a new function passed on a re-render is called again with every metric collected so far, which duplicates reports.

app/_components/web-vitals.tsxTSX
'use client'
import { useReportWebVitals } from 'next/web-vitals'
const report = (metric: { name: string; value: number; rating?: string; id: string }) => {
  const body = JSON.stringify(metric)
  if (navigator.sendBeacon) navigator.sendBeacon('/api/vitals', body)
  else fetch('/api/vitals', { body, method: 'POST', keepalive: true })
  console.log(`[vitals] ${metric.name} ${metric.value.toFixed(3)} ${metric.rating ?? ''}`)
}
export function WebVitals() {
  useReportWebVitals(report)
  return null
}

Render <WebVitals /> once in the root layout, above {children}, so the client boundary stays inside that one component. sendBeacon is the right transport: it hands the request to the browser and survives the page being unloaded, which matters because CLS and INP are reported exactly when the user leaves. A plain fetch without keepalive is routinely cancelled then, so the worst sessions are the ones you never hear about. Loading /slow under the same throttling printed this, and the same values reached the handler:

Output of 97
[vitals] FCP 428.000 good
[vitals] TTFB 4.700 good
[vitals] FID 9.200 good
[vitals] LCP 1880.000 good
[vitals] INP 272.000 needs-improvement
[vitals] CLS 0.084 good

Two other fields matter. id is unique to the page load, so you can rebuild percentile distributions instead of averaging, and navigationType separates navigate, reload, back-forward, back-forward-cache and restore - worth keeping, because back-forward-cache restores have near-zero LCP and flatter your averages.

Aggregate at the 75th percentile per route and per device class over a rolling 28 days, as the Chrome 1 User Experience Report does. Low-traffic sites never appear there at all, which is the argument for collecting your own data from day one.