How to Measure Core Web Vitals in Next.js
Checking your Core Web Vitals once - say, pasting a URL into PageSpeed Insights - tells you where you stand right now. It does not tell you how your users' experience changes over time. Deployments, traffic spikes, new content, third-party scripts - all of these shift your metrics in ways a single snapshot won't catch.
Whenever I join an existing project or kick off a new one, I make sure Web Vitals reporting is in place early. I want to see how real users experience the application over weeks and months, not just what a Lighthouse run on my local machine says on a Tuesday morning.
Next.js has built-in support for this through the useReportWebVitals hook, which reports metrics from real user sessions directly in your application code. Here's how to set it up.

Which metrics are included
The hook reports all standard Web Vitals:
| Metric | Full Name |
|---|---|
| TTFB | Time to First Byte |
| FCP | First Contentful Paint |
| LCP | Largest Contentful Paint |
| INP | Interaction to Next Paint |
| CLS | Cumulative Layout Shift |
In Core Web Vitals Explained I covered what each of these measures and what the thresholds are.
These are Real User Metrics (RUM) - collected from actual browser sessions, not synthetic lab tests. That distinction matters. Lab tools like Lighthouse run on a simulated device with a simulated connection. RUM data shows you what your actual users on their actual devices actually experience.
Sending metrics to an analytics endpoint
The useReportWebVitals hook takes a callback that fires for each metric. Since the hook requires the 'use client' directive, the recommended pattern is a separate component that you mount in your root layout.
One important detail: the callback function reference must not change between renders. Define it outside the component - not inline - to avoid duplicate reporting.
'use client'
import { useReportWebVitals } from 'next/web-vitals'
const reportWebVital = (metric) => {
const body = JSON.stringify(metric)
const url = 'https://analytics-example.com/api'
navigator.sendBeacon(url, body)
}
export function WebVitals() {
useReportWebVitals(reportWebVital)
return null
}
navigator.sendBeacon reliably delivers data even when the user is navigating away from the page, which is when most Web Vitals are finalized.
Replace the URL with your actual analytics endpoint. The metric object includes name, value, rating ("good", "needs-improvement", or "poor"), and a unique id per page load - enough to build dashboards and track regressions over time.
Sending metrics to Google Analytics
If you're already using Google Analytics, you can send Web Vitals as custom events via window.gtag:
'use client'
import { useReportWebVitals } from 'next/web-vitals'
const sendToGoogleAnalytics = (metric) => {
window.gtag('event', metric.name, {
value: Math.round(
metric.name === 'CLS' ? metric.value * 1000 : metric.value
),
event_label: metric.id,
non_interaction: true,
})
}
export function WebVitals() {
useReportWebVitals(sendToGoogleAnalytics)
return null
}
Two things to note here: CLS values are small decimals (e.g. 0.05), but Google Analytics event values must be integers - so the code multiplies CLS by 1000 before rounding. And non_interaction: true prevents these events from affecting your bounce rate calculations.
What's next
With real user metrics flowing in, you have the data to spot regressions and prioritize optimizations. The next step is acting on what the numbers tell you - which specific Next.js features and patterns actually move the needle on each metric.