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.
- 01Find the group one number hidesSplit your visits into segments, such as phones, one page template or first visits, and see which one is slow.
- 02Tell 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.
- 03Put 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.
- 04Not 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:
// 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:
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.
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.
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:
- Desktop, fast network44 ms, 8 visits
- Desktop, slow network1,212 ms, 8 visits
- Phone, fast network100 ms, 8 visits
- Phone, slow network1,228 ms, 8 visits
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.
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.
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:
// 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.
desktopFastis 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.
// 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:
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.
// 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
Mobiis a phone. Anything else is a desktop. - Line 6 prints
The laptop goes in the desktop segment.Line 7 printsThe iPhone goes in the phone segment.The iPhone's user agent saysMobile/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:
// 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 elseother. - 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:
// 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 printsProduct 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.
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.
// 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:
// 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 printsRelease b, everyone: p75 LCP 144 ms.
Now compare like with like: phones with phones, and desktops with desktops.
// 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.
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
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?
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:
// 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 msandVisits 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.
| Which split? | How can a beacon carry it? | What did it change here? |
|---|---|---|
| Device | Look for Mobi in the user agent: it means a phone. | On the fast network, the phone added 56 ms to INP. |
| Network | Read navigator.connection.effectiveType. Only Chromium browsers have it. | On the desktop, the slow network added 1168 ms to LCP. |
| Page template | Send 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 type | web-vitals puts navigationType on every metric. | Back and Reload were 3.3 and 3.4 times faster than a first visit. |
| First visit | Keep a note in localStorage, and send whether it was there. | A later visit was 3.4 times faster, with the same navigation type. |
| Login state | Send a loggedIn flag, true or false. Never send who. | Logging in added 251.9 ms to the p75 TTFB. |
| Country | The 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.
- 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.
Sort each pair by what made the difference.
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.
Here, release b has lost its desktop visits: line 3 now has an empty list. What does line 8 print?
// 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`);Release b, everyone: p75 LCP 1228 ms. Now release b's total is only phones, so it's 4 ms slower than release a's, not eight times faster. With four visits, that's still too few to judge.
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?
// 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`);Product page: p75 FCP 840 ms. That's only 32 ms after the home page's 808 ms, so the text painted at about the same time on both. The extra photos slowed the largest paint, not the first one.
In the playground, what was the p75 FCP for the phone on the slow network?
800 ms. Even the first text had to wait for the page itself to come over the slow network.
In the playground, what was the p75 LCP for logged-in visitors?
296 ms, 252 ms more than anonymous visitors' 44 ms. The server's wait came before anything could paint, so LCP grew by about as much as TTFB.
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?
3g. Its minimum round trip is 270 ms. A connection with quicker round trips counts as 4g, which is why most visits land there.
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?
restore. The README gives it to pages that were discarded by the browser and then restored by the visitor. Like Back and Reload, it's a kind of visit you can split out by its navigation type.
Check your understanding
For each question, ask: which segment is different, what changed between the two, and did the mix change too?
Question 1 of 7What does line 6 of the LCP example print?
Read the code, then predict// 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.
Question 2 of 7What does line 8 of the release totals example print?
Read the code, then predict// 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.
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.
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.
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.
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.
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:
- Emulation.setCPUThrottlingRate, Chrome DevTools Protocol.
- Network.emulateNetworkConditions, Chrome DevTools Protocol.
- Browser detection using the user agent, MDN.
- NetworkInformation: effectiveType property, MDN.
- CrUX dimensions, Chrome for Developers.
- Navigation Timing Level 2, W3C.
- web-vitals 6.2.3 README, Google Chrome.
- Window: localStorage property, MDN.
- Simpson's Paradox, Stanford Encyclopedia of Philosophy.
Coming next: Choosing a RUM tool. What commercial and open-source RUM tools offer, and the questions to ask before you pick one.