completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

Segmenting field data

Split data by device, network, country, page type, and navigation type to find who is slow and why. 120 loads of a shop in Chrome, on emulated phones and slow networks, cold and warm caches, logged in and not, and two releases whose overall numbers lie.

By the end, you can
  • 01
    Find the group one number hidesSplit your visits into segments, such as phones, one page template or first visits, and see which one is slow.
  • 02
    Tell one cause from anotherChange one thing at a time, so a slow network, a slow CPU, a heavy page and a busy server each show up on their own.
  • 03
    Put the right labels on each beaconSend the device, the network, the template, the navigation type, a first-visit flag and a logged-in flag, and never who the visitor is.
  • 04
    Not be fooled by the mixSpot Simpson's paradox, where a total gets better while no group does, and leave alone segments too small to judge.

Who is slow, and why?

Leaf and Kettle ships a new version of its mint tea page. The team calls it release b. The next day, a dashboard like the one in Building a RUM pipeline shows its number: a p75 LCP of 144 ms. The version before, release a, had 1224 ms.

Someone in the team says: "Release b is more than eight times faster!" Is it? And is anyone still waiting?

First, the two words in that number:

  • LCP, Largest Contentful Paint, is when the biggest image or block of text appeared. On this page, it's the mint tea photo.
  • The p75, the 75th percentile, is a time that three visits in four matched or beat.

Here are release b's 16 visits, in two groups:

One number for everyone, and one for phonesPop out in the code editor (opens in a new tab)JavaScript
// LCP of release b's 16 visits, in ms.const phones = [1228, 1228, 1224, 1232];const desktops = [132, 124, 120, 140, 128, 144, 128, 140, 124, 128, 132, 132]; const p75 = (values) => [...values].sort((x, y) => x - y)[Math.ceil(values.length * 0.75) - 1];console.log(`Everyone: p75 LCP ${p75([...phones, ...desktops])} ms`);console.log(`Phones only: p75 LCP ${p75(phones)} ms`);
  • Line 1 is a comment. Each number is one visit's LCP, in milliseconds (ms): thousandths of a second.
  • Line 2 holds the 4 visits from phones. Line 3 holds the 12 from desktop computers.
  • Line 5 works out a p75. It sorts a copy of the times, fastest first, and takes the one three quarters of the way along. For 16 visits, that's the 12th. For 4, it's the 3rd.
  • Line 6 puts both groups together. It prints Everyone: p75 LCP 144 ms.
  • Line 7 uses the phones alone. It prints Phones only: p75 LCP 1228 ms.

So the phone visits waited more than a second for the photo. Yet the number for everyone didn't show them at all. Why not? Put all 16 visits in a line, fastest first:

Release b's 16 visits in a line, fastest first. 12 desktop visits, from 120 to 144 ms, then 4 phone visits, from 1224 to 1232 ms. The p75 is visit 12: 144 ms.Desktops: 120 to 144 msPhones: 1224 to 1232 msp75: 144 ms, visit 12 of 16the fastest three quartersthe slowest quarter
Release b's 16 visits, fastest on the left. Shaded bars are phones. The p75 is visit 12, the slowest desktop. All 4 phone visits stand after it, in the slowest quarter, so the p75 never reaches them.

The p75 is the 12th visit in the line. The four phone visits were the four slowest: the 13th to the 16th. They filled the slowest quarter, and the p75 stopped just before them. A slow group of about a quarter of the visits, or fewer, can hide there completely.

Real-life analogyOne line for the whole school

A school sends parents one line about the exams: "Three pupils in four scored 60 or more." It's true. But one class, 9C, had no maths teacher for a term, and most of 9C scored under 40. The one line can't show that. The results class by class can. Then someone can ask what was different about 9C.

In real life: "Three pupils in four scored 60 or more"
In field data: One p75 for everyone: 144 ms
In real life: Class 9C, where most scored under 40
In field data: The 4 phone visits, at 1224 ms or more
In real life: The results, class by class
In field data: A p75 for each segment
In real life: Asking what was different about 9C
In field data: Asking what's different about phones

Splitting visits into groups like this is called segmenting. Each group is a segment: the visits that share one thing, such as phones, one kind of page, or first visits. Averages lie: reading distributions used the word first.

The main idea

One number for everyone tells you how slow, not who. Split the visits into segments, one thing at a time, and compare each segment with the same segment. The slow segment tells you who. What makes it different tells you why.

Here, phones are slow. But why? A phone can differ from a desktop in more than one way. It may have a slower processor, and a slower network. To tell those causes apart, change one thing at a time and watch what moves.

That's what the course did. It loaded a copy of the mint tea page 120 times in Chrome. Each visit loaded the page, clicked Add to cart, and left. As it left, the page sent all its metrics in one beacon, as in Sending performance data. The visits came in five experiments, and each one changed only one thing:

  • Device and network: a desktop or a phone, on a fast or a slow network.
  • Page templates: the home page, or a product page with more photos.
  • Cache and navigation type: a first visit, going Back, a reload, and a later visit.
  • Logged in or not: the same page, with and without a member's cookie.
  • Two releases: release a and release b, each with its own mix of phones and desktops. That's where the 144 ms came from.

Here is every experiment. Pick one, then a metric. Each row is a segment, with its p75 and how many visits it has:

Playground: split the data
Segmentp75 LCP
  1. Desktop, fast network44 ms, 8 visits
  2. Desktop, slow network1,212 ms, 8 visits
  3. Phone, fast network100 ms, 8 visits
  4. Phone, slow network1,228 ms, 8 visits
Try it yourself
Device and network: p75 LCP from 44 ms to 1,228 ms

Each row is one segment. The bar and the first number show its p75 LCP. The second number is how many visits it has: the fewer there are, the less you can trust the p75.

Recorded in Chrome 156 on Apple M1 Pro. The phones and the slow network are emulated through Chrome's DevTools protocol.

The next four sections take one kind of split each. The last one comes back to release b, and to the team's question.

Device and network segments

Where do you start splitting? Think of a café with a pile of complaints.

Real-life analogySorting the complaints before fixing anything

Leaf and Kettle's manager has a pile of complaints that say "too slow". Before she changes anything, she sorts them: by branch, by time of day, and by what was ordered. If most slow orders came from one branch, the cause is in that branch. If they were mostly toasted sandwiches, she looks at the toaster.

In real life: The pile of "too slow" complaints
In field data: Every visit's beacon
In real life: Sorting by branch, time of day, or what was ordered
In field data: Splitting by device, network, page template or country
In real life: One pile much bigger than the others
In field data: One segment much slower than the rest
In real life: The old toaster
In field data: The cause: a slow CPU, a slow network, a heavy page

Each way of sorting points at a different cause. This section sorts by the two things every visitor brings: a device and a network.

One thing at a time

The course made a phone and a slow network inside Chrome. It used Chrome's DevTools protocol, which lets a program steer Chrome from outside:

  • The phone had a screen 412 pixels wide. Its processor, the CPU, ran 4 times slower than the desktop's. The protocol calls this a "slowdown factor".
  • The slow network held every request for at least 300 ms before its answer began. All downloads together shared 200,000 bytes a second, about 200 KB.
  • The fast network had no delay and no limit.

It made 8 visits in each of the four mixes: a desktop or a phone, on the fast or the slow network. Here is what the network and the phone did to LCP:

What the network and the phone added to LCPPop out in the code editor (opens in a new tab)JavaScript
// p75 LCP from 8 visits in each segment, in ms.const desktopFast = 44;const desktopSlow = 1212;const phoneFast = 100; console.log(`The slow network added ${desktopSlow - desktopFast} ms to LCP.`);console.log(`The phone added ${phoneFast - desktopFast} ms to LCP.`);
  • Lines 2 to 4 are recorded: the p75 LCP of three segments. desktopFast is the desktop on the fast network, and so on.
  • Line 6 changes only the network: it compares the desktop with itself. It prints The slow network added 1168 ms to LCP.
  • Line 7 changes only the device: both are on the fast network. It prints The phone added 56 ms to LCP.

Each line compares two segments that differ in one way, so each difference has one cause. For loading the page, the network mattered more than twenty times as much as the phone.

Why so much? The mint tea photo is 102,809 bytes. On the slow network, the photo alone needed about half a second just to arrive, after a wait of at least 300 ms to start.

Now the click. Every visit pressed Add to cart, and the button runs a loop of 30,000,000 turns: the same work on every device. INP, Interaction to Next Paint, is the time from a click until the next frame shows its result.

What the network and the phone added to INPPop out in the code editor (opens in a new tab)JavaScript
// p75 INP from 8 visits in each segment, in ms.const desktopFast = 64;const desktopSlow = 64;const phoneFast = 120; console.log(`The slow network added ${desktopSlow - desktopFast} ms to INP.`);console.log(`The phone added ${phoneFast - desktopFast} ms to INP.`);
  • Lines 2 to 4 are recorded: the p75 INP of the same three segments.
  • Line 6 prints The slow network added 0 ms to INP.
  • Line 7 prints The phone added 56 ms to INP.

The click never touched the network. Its loop ran on the CPU, so only the slower CPU made it slower. Here are all four segments, with both metrics:

p75 by device and network: Desktop, Fast network: LCP 44 ms, INP 64 ms; Desktop, Slow network: LCP 1212 ms, INP 64 ms; Phone, Fast network: LCP 100 ms, INP 120 ms; Phone, Slow network: LCP 1228 ms, INP 120 ms.Fast networkSlow networkDesktopLCP 44, INP 64LCP 1212, INP 64PhoneLCP 100, INP 120LCP 1228, INP 120
Each card is one segment's p75, in ms. Going right, to the slow network, LCP went up. Going down, to the phone, INP went up.

Why it matters: "phones are slow" is not a cause. Split phones by network too. If LCP is slow only on slow networks, look at the bytes the page sends. If INP is slow on phones on every network, look at the code the click runs.

Putting the device and network on each beacon

In the recording, each page knew its segment, because the course put it in the page's address. A real site has to work it out.

Field data from every browser labelled each beacon's engine from its user agent: the text a browser sends to say what it is. The device can come from the same text. MDN's advice is short: "you can look for the string Mobi anywhere in the UA string." UA is short for user agent.

Which segment does each user agent go in?Pop out in the code editor (opens in a new tab)JavaScript
// Two user agents recorded in "Field data from every browser".const laptop = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.0.0 Safari/537.36";const iphone = "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1"; const deviceOf = (userAgent) => (userAgent.includes("Mobi") ? "phone" : "desktop");console.log(`The laptop goes in the ${deviceOf(laptop)} segment.`);console.log(`The iPhone goes in the ${deviceOf(iphone)} segment.`);
  • Lines 2 and 3 hold two user agents recorded in that lesson: Chrome on a laptop, and WebKit acting as an iPhone.
  • Line 5 is the test. Text that contains Mobi is a phone. Anything else is a desktop.
  • Line 6 prints The laptop goes in the desktop segment. Line 7 prints The iPhone goes in the phone segment. The iPhone's user agent says Mobile/15E148.

The network is harder to label. Chromium browsers have navigator.connection.effectiveType: the browser's own guess at the kind of connection. MDN lists its four values: slow-2g, 2g, 3g and 4g. The browser picks one from the round trips and download speeds it saw recently. Before you split by it, know two things:

  • Firefox and Safari don't have it. Their beacons have no answer, and no answer isn't the same as a fast or a slow one.
  • It's coarse. The docs of the Chrome User Experience Report say most visits "are likely on connections faster than 3G and thus classified as 4G". So it mostly picks out the slowest connections.

The Chrome User Experience Report, or CrUX, splits its data too. Its ways of splitting are called dimensions, and The Chrome User Experience Report covered them:

  • Form factor: phone, tablet or desktop, "inferred from the device User-Agent string".
  • Country: worked out from the visitor's IP address, the number the visit came from.

CrUX used to split by effective connection type too. It removed that dimension in February 2025, and reports a round-trip time instead.

Your own collector can find each beacon's country the same way, from the address the beacon came from. Keep the country and drop the address. Sending performance data showed why to store as little as you can.

Page templates and routes

Leaf and Kettle has a page for every tea: /tea/mint, /tea/oolong, and so on. Each of those paths is a route: the part of the address after the site's name. Every tea's page is built from the same page template: one layout, with a photo, a price and an Add to cart button.

Should you split by route, or by template? By template, for two reasons:

  • One route on its own may get only a few visits. A segment that small is noisy, as the last section shows. A template gathers the visits of all its routes.
  • The fix lives in the template. Make the product template faster, and every tea's page gets faster.

So put each route in its template before you split. A small function can do it:

Routes into templatesPop out in the code editor (opens in a new tab)JavaScript
// Every tea has its own page, but they all use one template.function templateOf(path) {  if (path === "/") return "home";  if (path.startsWith("/tea/")) return "product";  return "other";} console.log(`/tea/mint is a ${templateOf("/tea/mint")} page.`);console.log(`/tea/oolong is a ${templateOf("/tea/oolong")} page.`);console.log(`/ is the ${templateOf("/")} page.`);
  • Line 2 starts a function that takes a path.
  • Line 3 calls / the home page. Line 4 puts every path that starts with /tea/ in the product template. Line 5 calls anything else other.
  • Lines 8 to 10 print /tea/mint is a product page., /tea/oolong is a product page. and / is the home page.

The collector can run a function like this on each beacon's path. Or the page can send its template's name itself, since it knows which template built it.

Now the recording. The course's product page had the same photo as the home page, plus three more photos below it. It loaded each page 8 times, on the phone and the slow network:

The home page and a product pagePop out in the code editor (opens in a new tab)JavaScript
// LCP from 8 visits to each page, on a phone and a slow network, in ms.const home = [1224, 1228, 1220, 1232, 1216, 1228, 1232, 1228];const product = [2360, 2376, 2368, 2368, 2384, 2368, 2356, 2372]; const p75 = (values) => [...values].sort((x, y) => x - y)[Math.ceil(values.length * 0.75) - 1];console.log(`Home page: p75 LCP ${p75(home)} ms`);console.log(`Product page: p75 LCP ${p75(product)} ms`);
  • Lines 2 and 3 are recorded: the LCP of every visit to each page.
  • Line 5 is the same p75 as before.
  • Line 6 prints Home page: p75 LCP 1228 ms. Line 7 prints Product page: p75 LCP 2372 ms.

Same phone, same network, and the product page took almost twice as long. Why? Each photo is 102,809 bytes, so the product page's four photos add up to 411,236 bytes. Through 200,000 bytes a second, that's about two seconds of downloading. The photos shared the pipe, so each one arrived later.

Why it matters: a number for the whole site would only say "slow". Split by template, it says the product template is slow, and that's where the fix goes.

Cache state and navigation type

The first time Asha opens the mint tea page, her browser downloads everything. When she comes back, the browser can take the photo from its HTTP cache: a store of files it kept from earlier visits. That saves the download.

The course's server let the browser keep the photos and the web-vitals script for an hour. The page itself had to be fetched every time. On the phone and the slow network, the course made 6 rounds of four visits:

  1. A first visit, with an empty cache.
  2. Back: it followed a link away, then pressed the Back button.
  3. Reload: it reloaded the page.
  4. A later visit: it left again, then went to the page's address again, as a new visit.

How can a beacon tell these visits apart? Each page's navigation entry has a type: its navigation type. Navigation Timing names three:

  • "navigate": a normal visit, such as following a link or typing the address.
  • "reload": the page was reloaded.
  • "back_forward": "Navigation that's applied from history", such as the Back button.

The web-vitals library copies the type onto every metric, as navigationType. It writes back_forward with a hyphen: back-forward. It also has a few types of its own. You met "back-forward-cache" there: a page restored whole from the back/forward cache.

Here, going Back loaded the page again. Chrome didn't restore it from the back/forward cache, so web-vitals said "back-forward". Here is how the first three kinds of visit compare:

A first visit, Back and ReloadPop out in the code editor (opens in a new tab)JavaScript
// p75 LCP from 6 visits of each kind, on a phone and a slow network, in ms.const firstVisit = 1224;const back = 368;const reload = 364; console.log(`Back was ${(firstVisit / back).toFixed(1)} times faster than a first visit.`);console.log(`Reload was ${(firstVisit / reload).toFixed(1)} times faster than a first visit.`);
  • Lines 2 to 4 are recorded: the p75 LCP of each kind of visit, from 6 of each.
  • Line 6 divides the first visit's time by Back's. It prints Back was 3.3 times faster than a first visit.
  • Line 7 does the same for Reload. It prints Reload was 3.4 times faster than a first visit.

Why so much faster? The photo came from the cache. Only the page itself came over the slow network, and each request there waited at least 300 ms. That wait was most of each of these visits' LCP.

Now the fourth kind, the later visit:

Same navigation type, different speedPop out in the code editor (opens in a new tab)JavaScript
// Two kinds of visit, 6 of each, on a phone and a slow network. LCP in ms.const firstVisit = { navigationType: "navigate", p75Lcp: 1224 };const laterVisit = { navigationType: "navigate", p75Lcp: 360 }; console.log(`Same navigation type: ${firstVisit.navigationType === laterVisit.navigationType}`);console.log(`The later visit was ${(firstVisit.p75Lcp / laterVisit.p75Lcp).toFixed(1)} times faster.`);
  • Lines 2 and 3 are recorded. Both kinds of visit had the navigation type "navigate".
  • Line 5 prints Same navigation type: true.
  • Line 6 prints The later visit was 3.4 times faster.

The later visit was a normal visit too. Only the cache was different. So the navigation type alone can't tell a first visit from a later one.

Real-life analogyA regular and a first-time customer

A first-time customer at Leaf and Kettle reads the whole menu before ordering. A regular walks in, says "the usual", and her tea is on its way. They came in through the same door. If the café only times orders from the door, the regulars make it look quick while newcomers wait. So the till marks each order "new" or "regular".

In real life: The first-time customer, reading the whole menu
In field data: A first visit: every photo comes over the network
In real life: The regular who says "the usual"
In field data: A later visit: the photo comes from the cache
In real life: Both coming in through the same door
In field data: Both visits are navigate
In real life: Marking each order "new" or "regular"
In field data: Sending a first-visit flag with each beacon

Your beacons can do the same. The page can keep a small note in localStorage: storage the browser keeps for each site. MDN says its data "is saved across browser sessions". If the note isn't there, send firstVisit: true, then write the note.

Why it matters: a site with many returning visitors can look fast overall while first visits stay slow. And a first visit is a new customer's first look at the shop.

Logged-in vs anonymous users

Some visitors log in. Then the site knows who they are, and can build the page for them: their name, their cart, their usual tea. A visitor who hasn't logged in is anonymous. Every anonymous visitor can get the same page.

Think of a café that makes each member's tea fresh, to their own recipe, while everyone else gets tea from a pot that's already brewed. The member's tea is more personal. It also takes longer.

The course's server played this out. A cookie is a small piece of text a site stores in the browser. The browser sends it back with each request to that site. When a request carried a session cookie, the server waited 250 ms before sending the page. That wait stands in for building a page for one person.

The course loaded the page 8 times each way, on the desktop and the fast network. The metric to watch is TTFB, time to first byte: how long the browser waited for the first byte of the page.

The same page, anonymous and logged inPop out in the code editor (opens in a new tab)JavaScript
// TTFB from 8 visits each, on a desktop and a fast network, in ms.const anonymous = [2.3, 2, 1.4, 2, 1.3, 1.3, 1.7, 4.7];const loggedIn = [252.8, 253.7, 254.7, 252.9, 253.5, 253.8, 263, 253.9]; const p75 = (values) => [...values].sort((x, y) => x - y)[Math.ceil(values.length * 0.75) - 1];console.log(`Anonymous: p75 TTFB ${p75(anonymous)} ms`);console.log(`Logged in: p75 TTFB ${p75(loggedIn)} ms`);
  • Lines 2 and 3 are recorded: the TTFB of every visit.
  • Line 6 prints Anonymous: p75 TTFB 2 ms.
  • Line 7 prints Logged in: p75 TTFB 253.9 ms.

The gap is the server's 250 ms, and a little more. The device was the same, and so was the network. Only the server's work changed.

Why it matters: the visitors who log in can get the slowest pages, and only a split by login state shows it. So put a flag in each beacon: loggedIn: true or false. Never put in who they are. Sending performance data showed how much a beacon can carry without being asked.

Avoiding Simpson's paradox

Back to the team's question. Release a's p75 LCP was 1224 ms. Release b's was 144 ms. Is release b faster?

The course knew the answer before it started, because it made release b slower on purpose. Its server held release b's photo back 100 ms longer than release a's. Then each release got 16 visits, in a different mix:

  • Release a: 12 phone visits and 4 desktop visits.
  • Release b: 4 phone visits and 12 desktop visits.

The phones were on the slow network, and the desktops on the fast one. Here are the totals the dashboard showed:

Release a and release b, everyone togetherPop out in the code editor (opens in a new tab)JavaScript
// LCP of every visit, in ms. Release a went mostly to phones, release b mostly to desktops.const releaseA = { phones: [1224, 1216, 1232, 1228, 1216, 1224, 1216, 1224, 1224, 1220, 1224, 1228], desktops: [40, 36, 44, 40] };const releaseB = { phones: [1228, 1228, 1224, 1232], desktops: [132, 124, 120, 140, 128, 144, 128, 140, 124, 128, 132, 132] }; const p75 = (values) => [...values].sort((x, y) => x - y)[Math.ceil(values.length * 0.75) - 1];const everyone = (release) => [...release.phones, ...release.desktops];console.log(`Release a, everyone: p75 LCP ${p75(everyone(releaseA))} ms`);console.log(`Release b, everyone: p75 LCP ${p75(everyone(releaseB))} ms`);
  • Lines 2 and 3 are recorded: every visit's LCP, by release and device.
  • Line 6 puts each release's phones and desktops back together.
  • Line 7 prints Release a, everyone: p75 LCP 1224 ms. Line 8 prints Release b, everyone: p75 LCP 144 ms.

Now compare like with like: phones with phones, and desktops with desktops.

Release a and release b, device by devicePop out in the code editor (opens in a new tab)JavaScript
// LCP of every visit, in ms. Release a went mostly to phones, release b mostly to desktops.const releaseA = { phones: [1224, 1216, 1232, 1228, 1216, 1224, 1216, 1224, 1224, 1220, 1224, 1228], desktops: [40, 36, 44, 40] };const releaseB = { phones: [1228, 1228, 1224, 1232], desktops: [132, 124, 120, 140, 128, 144, 128, 140, 124, 128, 132, 132] }; const p75 = (values) => [...values].sort((x, y) => x - y)[Math.ceil(values.length * 0.75) - 1];console.log(`Phones: a ${p75(releaseA.phones)} ms, b ${p75(releaseB.phones)} ms`);console.log(`Desktops: a ${p75(releaseA.desktops)} ms, b ${p75(releaseB.desktops)} ms`);
  • Lines 1 to 5 are the same as in the last example.
  • Line 6 prints Phones: a 1224 ms, b 1228 ms.
  • Line 7 prints Desktops: a 40 ms, b 132 ms.

On desktops, release b was 92 ms slower. On phones, it was no faster. Yet its total was more than eight times better. How?

Look at who visited. Release b's visits were mostly desktops on the fast network, and those are quick. Release a's were mostly phones on the slow network. The totals mostly measured who visited, not how fast each release was.

Real-life analogyA quieter month at the café

Ravi times every order at Leaf and Kettle. In March, most customers come in the lunch rush. In April, most come in the quiet morning. Over all its customers, April's wait is shorter. But compare lunch with lunch, and morning with morning, and April was slower at both. The café didn't get faster. Its customers came at quieter times.

In real life: March's customers, mostly in the lunch rush
In field data: Release a's visits, mostly phones on a slow network
In real life: April's customers, mostly in the quiet morning
In field data: Release b's visits, mostly desktops on a fast network
In real life: April's shorter wait overall
In field data: Release b's lower p75 for everyone
In real life: Lunch against lunch, morning against morning
In field data: Phones against phones, desktops against desktops
p75 LCP of release a and release b: Everyone, a 1224 ms from 16 visits, b 144 ms from 16; Phones, a 1224 ms from 12 visits, b 1228 ms from 4; Desktops, a 40 ms from 4 visits, b 132 ms from 12.Everyonea: 1224 ms, 16 visitsb: 144 ms, 16 visitsPhonesa: 1224 ms, 12 visitsb: 1228 ms, 4 visitsDesktopsa: 40 ms, 4 visitsb: 132 ms, 12 visits
Shaded bars are release b. For everyone together, release b looks far faster. Yet its desktops were slower, and its phones no faster. Most of its visits were desktops.

This trap has a name: Simpson's paradox. The Stanford Encyclopedia of Philosophy puts it this way: "the type of association at the population level (positive, negative, independent) changes at the level of subpopulations." In plain words, what's true of everyone together can turn around inside each group. Here, release b looked better for everyone together. It was better for no group at all.

Averages lie: reading distributions showed the same trap with a monthly total. Now you've seen it in real visits.

Why it matters: when a total moves, check the mix before you check the speed. Show each segment's share of the visits next to its p75. If the shares changed, compare each segment with itself.

Four visits are too few to judge

One more look at those phones. Release b's phone p75 was 4 ms slower than release a's. Is that real?

Real-life analogyThree customers can't judge a new branch

A new Leaf and Kettle branch opens. On its first morning, three customers come in, and one waits a long time for her tea. Is the new branch slow? Three customers can't tell you. One slow cup changes the whole picture. Wait for a week of customers, then compare.

In real life: The new branch's three customers
In field data: Release b's 4 phone visits
In real life: One slow cup changing the whole picture
In field data: One slow visit moving a small segment's p75
In real life: A week of customers
In field data: Enough visits in each segment

Visits work the same way. Here are release a's own 12 phone visits, four at a time, in the order they happened:

The same release, four visits at a timePop out in the code editor (opens in a new tab)JavaScript
// Release a's 12 phone visits, in the order they happened: LCP in ms.const phones = [1224, 1216, 1232, 1228, 1216, 1224, 1216, 1224, 1224, 1220, 1224, 1228]; const p75 = (values) => [...values].sort((x, y) => x - y)[Math.ceil(values.length * 0.75) - 1];for (let start = 0; start < phones.length; start += 4) {  console.log(`Visits ${start + 1} to ${start + 4}: p75 LCP ${p75(phones.slice(start, start + 4))} ms`);}
  • Line 2 is recorded: release a's phone visits.
  • Line 5 steps through them four at a time. Line 6 prints the p75 of each group of four.
  • It prints Visits 1 to 4: p75 LCP 1228 ms, Visits 5 to 8: p75 LCP 1224 ms and Visits 9 to 12: p75 LCP 1224 ms.

Same release, same page, and nothing changed. Yet the p75 of four visits moved by 4 ms. Release a's first four phone visits even had the same p75 as release b's four: 1228 ms. So a 4 ms gap from four visits tells you nothing either way. Building a RUM pipeline saw a p75 of four visits jump around in the same way.

And the more ways you split, the smaller each segment gets. Phones, on a slow network, on the product page, logged in, in one country: few visits fit all five. Two habits help:

  • Show the count next to every p75. The playground does: it's the second number in each row.
  • Set a smallest size, and don't judge a segment below it. CrUX does this too. It publishes a country's numbers only with "the standard eligibility requirements applied at a country level". So each country needs enough page loads on its own.

How many visits are enough? Reading performance data works that out, with error bars.

Which split explains it?

Here is every split from this lesson in one table. Read each row across: the split, how a beacon can carry it, and what it changed in the recording.

Each split, how to send it, and what it changed
Which split?How can a beacon carry it?What did it change here?
DeviceLook for Mobi in the user agent: it means a phone.On the fast network, the phone added 56 ms to INP.
NetworkRead navigator.connection.effectiveType. Only Chromium browsers have it.On the desktop, the slow network added 1168 ms to LCP.
Page templateSend the template's name, or work it out from the path.The product page's p75 LCP was 1144 ms more than the home page's.
Navigation typeweb-vitals puts navigationType on every metric.Back and Reload were 3.3 and 3.4 times faster than a first visit.
First visitKeep a note in localStorage, and send whether it was there.A later visit was 3.4 times faster, with the same navigation type.
Login stateSend a loggedIn flag, true or false. Never send who.Logging in added 251.9 ms to the p75 TTFB.
CountryThe collector works it out from the beacon's IP address.Nothing here: every visit came from the same computer.

Now put them to work. Each item below is a pair of recorded numbers. Sort it by what was different between the two.

Which split explains it?
  • Same desktop, same page: p75 LCP 44 ms on one network and 1212 ms on another.
  • Same network, same click: p75 INP 64 ms on a desktop and 120 ms on a phone.
  • Same phone, same network: p75 LCP 1228 ms on the home page and 2372 ms on a product page.
  • Same phone, same page: p75 LCP 1224 ms on a first visit and 360 ms on a later one.
  • Same desktop, same page: p75 TTFB 2 ms when anonymous and 253.9 ms when logged in.
Try it yourself
0 of 5 correct

Sort each pair by what made the difference.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "One p75 for everyone shows who is slow." No. Release b's p75 of 144 ms hid four phone visits that took more than 1200 ms.
  • "Phones are slow because of their networks." No. On the same fast network, the phone's slower CPU added 56 ms to INP. The slow network added nothing to INP.
  • "Every route needs its own row." No. Group routes into templates. One route alone has few visits, and the fix lives in the template.
  • "The navigation type tells first visits from later ones." No. Both were "navigate", and the later visit was 3.4 times faster. Send a first-visit flag.
  • "A better total means a better release." No. Release b's total was more than eight times better, yet it was better for no group. Its mix of visits changed.
  • "Any segment can be trusted." No. Four visits are too few. Show the count beside each p75, and set a smallest size.
  • "CrUX splits by connection type." Not any more. It removed that dimension in February 2025. It still splits by form factor and country.

Practice exercises

Answer first, then use the hints. Exercises 3 and 4 use the playground in the first section. Exercises 5 and 6 send you to the docs.

Exercise 1 · Warm-upRelease b without its desktops

Here, release b has lost its desktop visits: line 3 now has an empty list. What does line 8 print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
// LCP of every visit, in ms. Release a went mostly to phones, release b mostly to desktops.
const releaseA = { phones: [1224, 1216, 1232, 1228, 1216, 1224, 1216, 1224, 1224, 1220, 1224, 1228], desktops: [40, 36, 44, 40] };
const releaseB = { phones: [1228, 1228, 1224, 1232], desktops: [] };

const p75 = (values) => [...values].sort((x, y) => x - y)[Math.ceil(values.length * 0.75) - 1];
const everyone = (release) => [...release.phones, ...release.desktops];
console.log(`Release a, everyone: p75 LCP ${p75(everyone(releaseA))} ms`);
console.log(`Release b, everyone: p75 LCP ${p75(everyone(releaseB))} ms`);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upThe first paint on each template

    This is the template example again, with each visit's FCP instead of its LCP. FCP, First Contentful Paint, is when the first text or image appeared. What does line 7 print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // FCP from 8 visits to each page, on a phone and a slow network, in ms.
    const home = [804, 792, 800, 808, 788, 812, 808, 796];
    const product = [832, 840, 856, 840, 852, 840, 836, 840];
    
    const p75 = (values) => [...values].sort((x, y) => x - y)[Math.ceil(values.length * 0.75) - 1];
    console.log(`Home page: p75 FCP ${p75(home)} ms`);
    console.log(`Product page: p75 FCP ${p75(product)} ms`);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeThe phone's first paint on the slow network

      In the playground, what was the p75 FCP for the phone on the slow network?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeLCP for logged-in visitors

        In the playground, what was the p75 LCP for logged-in visitors?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeReading the connection types

          effectiveType has four values. CrUX's dimensions page has a table of them, with the shortest round-trip time, the RTT, for each. Which value starts at an RTT of 270 ms?

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeOne more navigation type

            web-vitals has one more navigationType of its own: for a page the browser discarded, which the visitor then brought back. Find it in the web-vitals README. What is it?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask: which segment is different, what changed between the two, and did the mix change too?

              Segmenting field data quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does line 6 of the LCP example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // p75 LCP from 8 visits in each segment, in ms.
                const desktopFast = 44;
                const desktopSlow = 1212;
                const phoneFast = 100;
                
                console.log(`The slow network added ${desktopSlow - desktopFast} ms to LCP.`);
                console.log(`The phone added ${phoneFast - desktopFast} ms to LCP.`);

                Choose an answer to see the explanation.

              2. Question 2 of 7What does line 8 of the release totals example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // LCP of every visit, in ms. Release a went mostly to phones, release b mostly to desktops.
                const releaseA = { phones: [1224, 1216, 1232, 1228, 1216, 1224, 1216, 1224, 1224, 1220, 1224, 1228], desktops: [40, 36, 44, 40] };
                const releaseB = { phones: [1228, 1228, 1224, 1232], desktops: [132, 124, 120, 140, 128, 144, 128, 140, 124, 128, 132, 132] };
                
                const p75 = (values) => [...values].sort((x, y) => x - y)[Math.ceil(values.length * 0.75) - 1];
                const everyone = (release) => [...release.phones, ...release.desktops];
                console.log(`Release a, everyone: p75 LCP ${p75(everyone(releaseA))} ms`);
                console.log(`Release b, everyone: p75 LCP ${p75(everyone(releaseB))} ms`);

                Choose an answer to see the explanation.

              3. Question 3 of 7INP is slower on phones than on desktops, on the same network. What's the likely cause?

                Choose an answer to see the explanation.

              4. Question 4 of 7Logged-in visitors get a slower TTFB than anonymous ones, on the same device. Where do you look first?

                Choose an answer to see the explanation.

              5. Question 5 of 7Release b's total p75 LCP is far better than release a's, yet it's slower on desktops and no faster on phones. What happened?

                Choose an answer to see the explanation.

              6. Question 6 of 7A first visit and a later visit both have the navigationType "navigate", but the later one is far faster. What else should each beacon send?

                Choose an answer to see the explanation.

              7. Question 7 of 7A segment with four visits is 4 ms slower than last week. What do you conclude?

                Choose an answer to see the explanation.

              Key takeaways

              • One p75 for everyone can hide a slow group of about a quarter of the visits or fewer. Release b's 144 ms hid its phones.
              • Split one thing at a time: device, network, page template, navigation type, first visit, login state and country.
              • The slow network moved loading: it added 1168 ms to LCP. The phone's CPU moved the click: it added 56 ms to INP.
              • Group routes into templates, so each segment has enough visits and points at one fix.
              • The navigation type can't tell a first visit from a later one. Send a first-visit flag, and a logged-in flag, but never who.
              • When a total moves, check the mix before the speed. And show the count beside every p75.

              Remember the one-liner.
              Split one thing at a time, compare like with like, and count the visits before you judge.

              Sources

              The facts on this page come from these docs:

              Coming next: Choosing a RUM tool. What commercial and open-source RUM tools offer, and the questions to ask before you pick one.

              CompleteFrontend Clear concepts. Working examples.