Real user monitoring: measure your own visitors
Real user monitoring records the speed your visitors really get. What it adds to lab tests and CrUX, how the web-vitals library works, and how to use the data.
In this guide
What real user monitoring adds
You already have two kinds of data. A lab test such as Lighthouse loads a page once, under fixed conditions, and gives you a repeatable result with a list of causes. The Chrome UX Report gives you real-visitor data for the sites with enough traffic, collected from Chrome users, over a 28-day window. See lab and field data and the Chrome UX Report.
RUM is your own collection of field data. It adds:
- Your own pages. The report may cover your origin or a few popular URLs. RUM covers every page that gets visits.
- Your own labels. You can split the data by page type, template, country, device, login state or a campaign.
- Causes. An attribution build can tell you which element was the largest, which script ran during the slow tap, and what state the page was in.
- Speed. You see the effect of a change in days, not after a rolling 28-day window has passed.
- Other browsers, wherever the browser interfaces exist.
It also costs something. A script runs on your pages, it collects data you must store and read, and it has limits. It supplements the other tools. It does not replace them.
The web-vitals library
The Chrome team's web-vitals library is a small, modular library for measuring the Web Vitals on real users. Its documentation says it measures them in a way that accurately matches how Chrome measures them and how they are reported to other Google tools, such as the Chrome UX Report, PageSpeed Insights and Search Console. It measures:
- Largest Contentful Paint (
onLCP), Interaction to Next Paint (onINP) and Cumulative Layout Shift (onCLS), the Core Web Vitals; - First Contentful Paint (
onFCP) and Time to First Byte (onTTFB), which help to diagnose.
The core is about 3 KB compressed with Brotli. Install it from npm or load it from a CDN. Each function takes a callback that is called when the value is ready. This example sends each result to your own endpoint:
import {onCLS, onINP, onLCP} from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
id: metric.id,
navigationType: metric.navigationType,
page: location.pathname,
});
// sendBeacon works while the page is closing
navigator.sendBeacon('/analytics', body);
}
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);This follows the library's own example, with a few extra labels. The /analytics address is yours to provide. It can be a function on your server, a worker, or an analytics service that accepts events.
What to know before you trust the numbers
The documentation lists several behaviors:
- Some values arrive late. Some metrics are reported only after the visitor interacts, switches tabs, or leaves the page. If you do not see a value at once in the console, that is why.
- Some values never arrive. INP is not reported if the visitor never interacts. CLS, FCP and LCP are not reported if the page loaded in the background.
- Some arrive twice. CLS and INP should be reported whenever the page becomes hidden. All metrics are reported again after the page is restored from the back/forward cache. Each report has an
id, which lets you tell instances apart. See the back/forward cache. - Do not call the functions again and again on the same page load. Each creates an observer that costs memory and work.
- `reportAllChanges` is for debugging. It reports each change in the value. The documentation says it is not recommended in production.
- Iframes are invisible. The browser interfaces cannot see inside iframes, not even same-origin ones. A page with embedded frames may show different numbers from the Chrome UX Report, which does include them. For the same reason, on a page with iframes, the layout shift figure is technically a document-level one.
- Browser support varies. According to the README,
onCLSis for Chromium, whileonFCP,onINP,onLCPandonTTFBwork in Chromium, Firefox and Safari.
The navigation type
Each metric carries a navigationType. It is the value from the Navigation Timing interface, with some additions: back-forward-cache for a page restored from the back/forward cache, prerender for a page that was prerendered, restore for a page the browser discarded and the visitor restored, and soft-navigation for soft navigations. Keep it as a label. A restored page will have a very fast value, and mixing it in without a label will make the average look better than a fresh load. See speculation rules.
Find the cause with attribution
The library has an attribution build, which adds data to each report to help you diagnose why a value is what it is. For LCP it names the element. For CLS, the element that shifted most. For INP, the element involved in the slowest interaction and a breakdown into input delay, processing time and presentation delay. Send these along with the number. The README example sends them as extra parameters to Google Analytics:
import {onCLS, onINP, onLCP} from 'web-vitals/attribution';
onINP(({name, value, attribution}) => {
navigator.sendBeacon('/analytics', JSON.stringify({
name,
value,
element: attribution.interactionTarget,
}));
});The README says the attribution build is slightly larger than the standard one, by about 1.5 KB compressed, so use it only if you will use the extra data. Its INP data also includes a load-state value, which tells you whether the page was still loading when the slow interaction happened. If slow interactions happen while the page is loading, the cause is start-up work. See how to fix INP.
Make the data useful
- Choose the labels first. Page template, device class, country, navigation type. Without labels, a single average hides the pages that fail.
- Read a percentile, not an average. The Core Web Vitals use the 75th percentile of visits. Group your data by page type and look at the 75th percentile of each metric. The library gives thresholds: LCP 2,500 and 4,000 milliseconds, INP 200 and 500 milliseconds, CLS 0.1 and 0.25. Each report also carries a
ratingof good, needs improvement or poor, so you do not have to compute it. - Watch the trend, not single days. Visitors, devices and campaigns change from day to day.
- Compare with CrUX. If your numbers differ a lot from the Chrome UX Report's, check for iframes, for browsers CrUX does not cover, and for how you sample.
- Tie changes to releases. Note the date of each deploy, plugin change and new script, and look at the metrics around it.
Privacy and weight
- Collect only what you need. Speed metrics do not need names, emails or exact addresses. The sample code sends the page path, the value and a label.
- Follow your privacy rules. Check what your policy and local rules require before you add any collection script.
- Keep the script small and late. Load it with
deferor as a module, and do not let it compete with the main content. The core library is small, but the analytics endpoint and the storage are your cost. - Sample if the traffic is large. Sending a fraction of visits is enough for a stable percentile on busy pages.
Mistakes to avoid
- Treating the average as the result. Use percentiles and labels.
- Counting a restore as a load. Keep the navigation type.
- Collecting everything and reading nothing. Decide the questions first.
- Forgetting the limits. No iframe content, and no INP without an interaction.
- Replacing lab tests with RUM. Use RUM to find which pages and visitors suffer, and lab tools to find the cause and test the fix.
Check your own page
Run the free audit for a lab result, then compare it with the real-visitor numbers for the same page in PageSpeed Insights. If you do not have enough traffic for the Chrome UX Report, or you need page-level answers, add the web-vitals library and collect your own.
Sources
- The web-vitals library: the README with the metrics, size, reporting behavior, the
Metricobject and navigation types, thresholds, build options, browser support and limitations. - Chrome UX Report methodology in the Chrome documentation: what the report covers and over what period.
- Navigator: sendBeacon() on MDN: the method intended for sending analytics data.
Start here
Free website audit
Live Google data for any public page: mobile and desktop.
Mobile + desktop · Lighthouse lab test · Real-user Chrome data when Google has it
Rather have this done for you? Core Web Vitals · Book a call