Tutorial · performance · Published 2026-08-16 · 3 min read

Measuring Core Web Vitals with the web-vitals JavaScript library

Using the web-vitals JavaScript library to measure LCP, INP and CLS, send field data to analytics and debug the metrics in a real browser.

Why measure on real users

The Core Web Vitals that matter for ranking come from field data: what real visitors experienced, not what a lab machine reported. The official web-vitals npm package measures those same metrics in the browser and hands you the values to send wherever you keep analytics. It reports LCP, INP and CLS exactly as Chrome measures them, including the buffering rules that capture the final CLS even when it happens after the page is visible.

Add the library

Install the package and load it at the top of the page body:

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

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

The <head> matters: the earlier the script runs, the more likely the callback captures the start time for LCP and the interactivity window for INP. Load the script as type="module" and do not defer it, because INP and CLS buffers only begin with the first script run.

Send to analytics

function sendToAnalytics(metric) {
  const body = {
    name: metric.name,
    value: metric.value,
    id: metric.id,
    delta: metric.delta,
    rating: metric.rating,
  };
  navigator.sendBeacon('/analytics/web-vitals', JSON.stringify(body));
}

The web-vitals package exposes a reportAllChanges flag on the metric options; leave it disabled (default) or the CLS counter fires on every frame of layout shift instead of the final value. A beacon endpoint can be any small server route that your analytics or logging pipeline consumes.

Send the metrics, not just a number

Field data without segmentation is a number with no owner. Send at least:

A config report that sends name/value/delta to your BI renders a graph; the same payload sent without id collapses every load into one row and loses the ability to see the 75th-percentile story the report wants.

The four traps of measuring in prod

  1. LCP on a cached page. If the LCP candidate is served from memory cache, the browser reports a value of 0 or a minuscule number. Filter delta/value at the endpoint for value > 0 before averaging.
  2. CLS ignores the above-buffer. A CLS after firsthidden is not counted; the library handles that correctly, but a lab tool that recalculates CLS from a screenshot can over-report. Trust the library over your bug-finder.
  3. Bots and localhost. Exclude crawlers and your own team by user-agent and by navigator.webdriver, or your 95th percentile turns into a mix of bots and the staging box.
  4. Double reporting. If a tag manager injects the script into a SPA, each route may re-run the callbacks. The id guard dedupes.

Verify the wiring

In DevTools, block your beacon endpoint, then load the site and check the console: the library logs to console.debug when its debug option is enabled at metric registration, showing the computed metric values. A real user's page visit must then produce a row in your analytics, not an empty report.

The three metrics are explained in the Core Web Vitals reference, the audit procedure walks the debug order when the numbers look wrong, and the PageSpeed article pairs field data with the lab run that reproduces a single page view.

Need a website built, fixed, optimised, migrated or replaced?

This technical resource is written by CSMBAC, a small design and development studio. If you would rather hand the problem to a professional, the website service page explains how we build enquiry-ready websites.

Explore website services