RUM Accuracy Starts With How You Collect the Data

Field data is the only way to confirm that performance work on your site is producing real results for users. If your current Real User Monitoring (RUM) or analytics setup already supports the Core Web Vitals metrics, you're set. If it doesn't, you don't necessarily need to migrate to a new vendor. Nearly every analytics tool supports custom metrics or events, which is enough to measure Core Web Vitals yourself.

This is a practical guide for engineering teams who want to instrument Core Web Vitals (or any custom performance metric) through their existing analytics stack, and it also applies to analytics vendors building Core Web Vitals support into their products.

Instrumenting metrics with custom events

Measuring any custom metric in an analytics tool generally follows a three-step process:

  1. Define or register the metric in the tool's admin console (some providers skip this requirement).
  2. Compute the metric's value in frontend JavaScript.
  3. Send that value to the analytics backend, matching the name or ID from the first step.

For the first and last steps, your analytics provider's documentation is the source of truth. For the computation step, the web-vitals JavaScript library handles the underlying API logic. Here is a minimal example of tracking each Core Web Vital and sending it to an analytics service:

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

function sendToAnalytics({name, value, id}) {
  const body = JSON.stringify({name, value, id});
  // Use `navigator.sendBeacon()` if available, falling back to `fetch()`.
  (navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) ||
      fetch('/analytics', {body, method: 'POST', keepalive: true});
}

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

Don't average; report percentiles

Averages are an intuitive way to summarize performance data, but they don't represent a single user's session. Outliers at either end of a distribution can skew a mean so that it suggests a problem where none exists, or hides one where it does—for instance, a small set of users on very slow connections may not move the average enough to flag a real issue.

Report percentiles instead. For a given metric, percentiles describe the full range of experiences. Focusing on subsets of actual users gives you more actionable insight than any single aggregate value can.

Getting to a 75th-percentile report

After you've collected metric values, you need a report or dashboard that shows whether each Core Web Vital passes its recommended threshold at the 75th percentile. If your analytics tool doesn't offer quantile calculations natively, generate a report with every metric value sorted in ascending order; the value 75% of the way through that sorted list is your 75th percentile—and this holds regardless of how you segment the data.

For tools that lack metric-level granularity, custom dimensions can help. Assign a unique custom dimension value to every individual metric instance. Then, when you build a report that includes that dimension, each instance occupies its own row and no grouping happens, giving you the fine-grained data you need to calculate the percentile yourself.

When to send data for page-lifetime metrics

Some metrics—like Largest Contentful Paint (LCP)—are final after page load, but Cumulative Layout Shift (CLS) and others consider the entire page lifetime. The beforeunload and unload events are not reliable callbacks for this purpose, especially on mobile, and their use can make a page ineligible for the Back-Forward Cache.

For metrics that track the full page lifespan, send whatever value you have in a visibilitychange event when the page's visibility state becomes hidden. Once that state change fires, there's no guarantee any script will run again—the browser app itself can be closed without further callbacks. Mobile operating systems reliably fire the visibilitychange event for tab switches, app switches, and navigation, making it far more dependable than the legacy unload APIs.

Tracking performance over time

Once your instrumentation and reports are live, you need a way to evaluate how site changes affect performance.

Version your deployments

Assuming that metrics before a deploy date belong to the old version and metrics after it belong to the new version is unreliable, because HTTP caches, service workers, and CDNs all distort that timeline. Attach a unique version string to each deploy and record it in analytics—either via a built-in version field or a custom dimension.

Run controlled experiments

Versioning alone is limited; running multiple versions simultaneously with a control group reveals real causal impact. Use your analytics tool's experiment-group feature if it exists, or a custom dimension to associate each metric value with a group. Roll an experimental change out to a subset of users and compare their performance data against the control group. Only when the experiment group shows a convincing improvement should you push the change to everyone.

Keep measurement overhead near zero

Your own measurement code must not worsen page performance. If it does, you'll never know whether regressions come from real site changes or from the instrumentation itself. Follow these principles when shipping RUM code to production.

Load analytics last, asynchronously

Blocking analytics code can directly inflate LCP. The APIs behind the Core Web Vitals metrics all support deferred, asynchronous scripts through the buffered flag, so there's no need to load early. If a single metric needs early measurement, inline only the small amount of code required in the document <head> (avoiding a render-blocking request) and keep the rest deferred.

Avoid heavy main-thread work

Analytics that runs on user input should not create long tasks. Heavy DOM reads or large JavaScript payloads that need to parse and execute on the main thread can hurt Interaction to Next Paint (INP) and input responsiveness.

Use purpose-built APIs

sendBeacon() and requestIdleCallback() exist for exactly this kind of non-critical work. Send analytics beacons with sendBeacon() when it's available, and schedule passive measurement work for idle periods rather than running it inline with critical tasks.

Collect only what you'll use

The browser exposes a large surface of performance data—Resource Timing, for example—but that doesn't mean you should transmit all of it. Sending data you won't act on consumes network, storage, and battery for no benefit. Before you record a metric, confirm the data will drive a decision.