Lab tests tell you how a page behaves under controlled conditions; real user monitoring, or RUM, tells you how it actually behaves on visitors' devices and networks. There are commercial services that offer it, but a basic system can be built with a few lines of code and an endpoint that receives the data. The advantage is complete control: you choose what to collect, where to store it and how to combine it with information about your own site.
The browser APIs
The heart of RUM is PerformanceObserver, an interface that notifies your code of performance entries as the browser records them. Each entry type corresponds to a different phenomenon:
| Entry type | What it describes |
|---|---|
navigation |
Document load timings, including TTFB |
resource |
Timings for every downloaded resource |
largest-contentful-paint |
Candidates for the largest painted element |
layout-shift |
Individual layout shifts |
event |
Interaction durations, the basis of INP |
long-animation-frame |
Slow frames with script attribution |
A direct example for LCP:
new PerformanceObserver((list) => {
const entries = list.getEntries();
const last = entries[entries.length - 1];
console.log('LCP candidate:', last.startTime, last.element);
}).observe({ type: 'largest-contentful-paint', buffered: true });
The buffered: true option also retrieves entries recorded before the observer was created, which lets you load the measurement script asynchronously without losing data.
Why use the web-vitals library
Computing the metrics correctly is harder than it looks. LCP must stop at the user's first interaction; CLS is computed over session windows, not as a simple sum; INP requires grouping events by interaction and picking a value close to the worst; pages restored from the bfcache or opened in the background need special handling.
The web-vitals library, maintained by the Chrome team, implements all these rules the same way the official tools do. Using it is simple:
import { onLCP, onINP, onCLS, onTTFB } from 'web-vitals';
function send(metric) {
const data = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
id: metric.id,
page: location.pathname,
});
navigator.sendBeacon('/rum', data);
}
onLCP(send);
onINP(send);
onCLS(send);
onTTFB(send);
The functions do not return the value immediately: they report it when it is final, usually when the page is hidden. That is why sending uses sendBeacon, which completes the request even if the tab is closed.
The attribution build
Knowing that INP is high is not enough: you need to know why. The library offers an attribution build that adds diagnostic details: for LCP the element involved and the breakdown of the time into its phases, for INP the selector of the element the interaction happened on and the three components of latency, for CLS the element that shifted the most. This is the information that turns a number into a concrete fix.
Enriching the data
An isolated metric says little. It pays to attach a few useful dimensions to each report:
- page type (home, product page, article), more useful than the individual URL;
- device type, derived from screen width;
- navigation type: normal load, reload, bfcache restore, prerender;
- connection quality, where the Network Information API is available;
- site version, to compare before and after a release.
Be careful not to collect personal data: full IP addresses, URL parameters with identifiers or typed text are not needed to measure performance.
Analysing by percentile
Performance values do not follow a symmetrical distribution: most visits are fast, but a tail of slow visits can be very long. The average hides that tail, which is why Core Web Vitals are assessed at the 75th percentile: the value below which 75% of visits fall.
A useful analysis includes:
- the 75th percentile of each metric by page type and device;
- the distribution across the three bands: good, needs improvement, poor;
- the trend over time, to spot regressions after a release;
- for the worst pages, the most frequent elements in the attribution data.
With a database that accepts SQL queries, these views take just a few queries to build. In my work, a minimal RUM setup has often revealed problems invisible in the lab, such as a banner that shifted the layout only on certain phone models.
Sampling
On high-traffic sites there is no need to collect every visit. Deciding at the start of the page, with a fixed probability, whether the session will be measured reduces data volume while keeping statistics reliable. What matters is that the decision applies to the whole visit, not to each metric separately.
In short
PerformanceObserverexposes the performance entries recorded by the browser.- The web-vitals library computes LCP, INP and CLS with the same rules as the official tools.
- Send data with
sendBeaconand use the attribution build to understand causes. - Enrich measurements with page type, device and navigation type, without personal data.
- Analyse the 75th percentile and the trend over time, not the average.