completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

From field data to a fix

Use attribution to find the cause, reproduce it in the lab, fix it, and confirm the change in the field. 83 visits of a shop in Chrome: field data with web-vitals attribution, a lab that matches the slow segment, two fixes, and the field again.

By the end, you can
  • 01
    Follow the clues in attribution dataRead a slow segment's notes to find the element, the part of the metric that was slow, and the script that ran.
  • 02
    Make the problem happen in the labSet the lab's CPU and network like the slow segment's, and keep trying until the numbers agree.
  • 03
    Fix it, then prove it with real usersChange the cause the clues named, and compare the same segments in the field, before and after.

From field data to a fix

Leaf and Kettle's team has done the hard part of field data. Every visit to the mint tea page sends its metrics home. The dashboard splits them into segments, the way Segmenting field data showed. This morning, it shows who is slow:

Who is slow, from the fieldPop out in the code editor (opens in a new tab)JavaScript
// p75 from the field, release a, in ms. Good is an LCP of 2500 or less and an INP of 200 or less.const field = [  { segment: "desktop, fast", LCP: 1064, INP: 168 },  { segment: "desktop, slow", LCP: 2588, INP: 168 },  { segment: "phone, fast", LCP: 1088, INP: 584 },  { segment: "phone, slow", LCP: 2608, INP: 576 },];const slow = (metric, limit) => field.filter((row) => row[metric] > limit).map((row) => row.segment).join(" and ");console.log(`Slow LCP: ${slow("LCP", 2500)}`);console.log(`Slow INP: ${slow("INP", 200)}`);
  • Line 1 is a comment. LCP, Largest Contentful Paint, is when the biggest image or block of text appeared. Here, that's the mint tea photo. INP, Interaction to Next Paint, is how long the page took to show the result of a click, like Add to cart.
  • Each number is a p75, the 75th percentile: a time that three visits in four matched or beat. It's in milliseconds (ms), thousandths of a second. web.dev calls an LCP good at 2.5 seconds or less, and an INP at 200 ms or less.
  • Lines 2 to 7 hold four segments. Each one is a desktop or a phone, on a fast or a slow network.
  • Line 8 keeps the segments that are over a limit, and joins their names.
  • Line 9 prints Slow LCP: desktop, slow and phone, slow. Line 10 prints Slow INP: phone, fast and phone, slow.

So the slow network hurts LCP, and the phone hurts INP. That's who. But the dashboard can't say why. What kept the photo waiting? What did the click do for more than half a second? And once the team changes something, how will they know it helped?

This lesson answers those three questions in four steps:

  1. Read the attribution: a note each report carries about where its time went.
  2. Reproduce the problem in the lab, so you can watch it happen.
  3. Fix the cause the note and the lab point at.
  4. Confirm the fix in the field, with the same segments as before.
From field data to a fix: Field data, who is slow; then Attribution, what to look at; then Lab, see it happen; then Fix, change the cause; then Field again, same segments.Field datawho is slowAttributionwhat to look atLabsee it happenFixchange the causeField againsame segments
Read the field, follow the attribution, watch it happen in the lab, fix the cause, and check the field again. The last step uses the same segments as the first.
Real-life analogyA doctor hears where it hurts, then watches it happen

You tell a doctor: "My knee hurts on the stairs at home." She can't see your stairs. So she asks you to climb the clinic's stairs while she watches. Now she can see what goes wrong, and treat that, not just the pain.

In real life: "It hurts on the stairs at home"
In performance work: Field data: phones and slow networks are slow
In real life: She can't see your stairs
In performance work: Field data can't replay the moment it happened
In real life: Climbing the clinic's stairs while she watches
In performance work: Making the slow visit happen in the lab
In real life: Treating what she saw
In performance work: Fixing the cause, not the number
The main idea

Field data says where it hurts. Attribution says what to look at. A lab set up like the slow segment lets you watch it happen. Fix that cause, then check the same segments in the field. Read, reproduce, fix, confirm.

A shop with two hidden problems

To follow the steps with real numbers, the course built its own copy of the mint tea page. It put two problems in it on purpose. Try not to guess them yet: the clues will lead you there.

The shop comes in two releases, two versions of its code. Release a has the problems. Release b has the fixes. Each visit loaded the page, waited for the photo, clicked Add to cart, and left. As it left, the page sent its metrics home in a beacon, the small parting message from Sending performance data. It measured them with the web-vitals library's attribution build. The course made two kinds of visits:

  • The field: 48 visits standing in for real users. Each release had 6 visits in each of the four segments. The phones and slow networks were made inside Chrome, the same way as in Segmenting field data.
  • The lab: 35 runs, the kind you'd do at your own desk: 7 setups, 5 runs each.

Here are all 83. Start with Field, before, and switch between LCP and INP. Each row is one segment's p75. Under it is the biggest part of that visit, from its note.

Playground: from field to fix
Segmentp75 LCP
  1. Desktop, fast network1,064 msresourceLoadDelay 1,040 ms
  2. Desktop, slow network2,588 msresourceLoadDelay 1,736.6 ms
  3. Phone, fast network1,088 msresourceLoadDelay 1,060.8 ms
  4. Phone, slow network2,608 msresourceLoadDelay 1,760.9 ms
Try it yourself
Field, before: the slowest p75 LCP is 2,608 ms, in the row "Phone, slow network"

Each row is one segment. The bar is its p75 LCP, from 6 visits. Under it is the biggest part of the visit at that p75, from web-vitals' attribution.

Recorded in Chrome 156 on an Apple M1 Pro. The phones and slow networks were made inside Chrome, through its DevTools protocol.

Reading attribution data

Start with the photo. Phones on the slow network had a p75 LCP of 2,608 ms. Why?

A number alone can't say. But each report from the attribution build carries a note. In The web-vitals library, that note was a report card that says why: not just "Maths: C", but where the marks were lost. Reading it is like following a trail of clues.

Real-life analogyFollowing the clues from a complaint to its cause

A customer at Leaf and Kettle says: "My tea took ages." The manager pulls the order slip. It shows the table, and that the order sat at the till for ten minutes. The till's log shows why: it was printing the day's report. Each clue points to the next, until she finds the one thing to change.

In real life: "My tea took ages"
In attribution: A slow p75 in one segment
In real life: The order slip: which table, which step was slow
In attribution: The note: which element, which part of the metric
In real life: The till's log: printing the day's report
In attribution: longestScript: the function that was running
In real life: The one thing to change
In attribution: The line of code behind it

Which visit to read

A segment has many visits, and each one has its own note. This lesson reads one: the visit at the p75 itself. It's the circled wait on the tally sheet from Building a RUM pipeline. That makes it a real visit, at the slow edge of what most people got.

With thousands of visits, you'd count the notes instead. For example: which element was the LCP most often? web.dev's guide to debugging in the field says to start with the issues "that are affecting the largest number of users."

LCP, in four parts

LCP's note splits its time into four parts, one after the other:

  • timeToFirstByte: from the start of the visit to the page's first byte.
  • resourceLoadDelay: from that first byte until the browser starts loading the photo.
  • resourceLoadDuration: the photo's own download.
  • elementRenderDelay: from the photo arriving until it's painted.

Here is the note from the p75 visit of phones on the slow network:

Where the slow segment's LCP wentPop out in the code editor (opens in a new tab)JavaScript
// web-vitals' LCP attribution for the p75 visit of phones on the slow network, release a, in ms.const lcp = { target: "#hero", timeToFirstByte: 2.7, resourceLoadDelay: 1760.9, resourceLoadDuration: 821.1, elementRenderDelay: 23.3 }; const { target, ...subparts } = lcp;const total = Object.values(subparts).reduce((sum, ms) => sum + ms, 0);console.log(`${target}: LCP ${Math.round(total)} ms`);for (const [name, ms] of Object.entries(subparts)) console.log(`${name}: ${Math.round((ms / total) * 100)}%`);
  • Line 2 is the note, as Chrome recorded it. Its target is the element that was the LCP: #hero, the mint tea photo.
  • Line 4 sets the target aside and keeps the four parts.
  • Line 5 adds them up. Line 6 prints #hero: LCP 2608 ms. The four parts make up the whole LCP.
  • Line 7 prints each part as a share of it: timeToFirstByte: 0%, resourceLoadDelay: 68%, resourceLoadDuration: 31% and elementRenderDelay: 1%.

Two thirds of the wait was resource load delay. The web-vitals README defines it as "The delta between TTFB and when the browser starts loading the LCP resource". In plain words: the page had started to arrive, and the photo hadn't even been asked for.

web.dev's LCP guide points out that two of the parts have "delay" in their names. It says: "That is a clue that you want to get these times as close to zero as possible." Its table of healthy shares puts resource load delay under 10%. Here it was 68%.

Why it matters: a smaller photo would only touch the 31% spent downloading it. The bigger problem is that it started late. So why did it start late?

One more thing about line 2. Its time to first byte is tiny, 2.7 ms, even on a slow network. The HTML's first byte arrived at 2.7 ms, but its last byte only at 326.7 ms. In these recordings, Chrome's slow network spent its time inside the HTML's download, after the first byte.

The clue leads to the code

The note also gave the photo's address, hero.jpg. So where does the page ask for it? Release a's HTML has no <img> for the photo. It has only an empty <div id="hero-slot">. A script at the end of the page fills it in:

How release a shows the photoJavaScript
fetch("/api/hero")  .then((response) => response.json())  .then(({ src, alt }) => {    const img = new Image(412, 275);    img.id = "hero";    img.src = src;    img.alt = alt;    document.querySelector("#hero-slot").append(img);  });
  • Line 1 asks the shop's server which photo to show, at /api/hero. An API here is an address that answers with data, not with a page.
  • Lines 2 and 3 wait for the answer and read the photo's address from it.
  • Line 4 makes a new image element. Line 6 gives it the address. Only now can the browser start the download.
  • Line 8 puts the image into the empty slot.

The course's API took 1,000 ms to answer, on purpose, like a slow content service. So the photo couldn't start until the answer came. That's where the resource load delay came from.

INP, in three parts

Now the click. INP's note splits the slow click into the three waits from the café counter in Event Timing and INP:

  • inputDelay: waiting for the cook to hear the bell, before the click's code starts.
  • processingDuration: the cook making the tea, while the click's code runs.
  • presentationDelay: waiting for the waiter's round, until the next frame is painted.
Where the slow segment's INP wentPop out in the code editor (opens in a new tab)JavaScript
// web-vitals' INP attribution for the p75 visit of phones on the slow network, release a, in ms.const inp = { target: "#cart", inputDelay: 0.7, processingDuration: 533.7, presentationDelay: 41.6 }; const { target, ...phases } = inp;const [phase, ms] = Object.entries(phases).sort((a, b) => b[1] - a[1])[0];console.log(`${target}: ${phase} ${ms} ms`);
  • Line 2 is the note from the p75 visit of phones on the slow network. Its target, #cart, is the Add to cart button.
  • Line 4 keeps the three phases. Line 5 sorts them, biggest first, and keeps the first.
  • Line 6 prints #cart: processingDuration 533.7 ms.

An event listener is a function the page runs when an event, like a click, happens. Processing duration is "The time from when the first event listener started running in response to the user interaction until when all event listener processing has finished." So the shop's own click code ran for more than half a second. The cook took ages. The bell and the waiter were quick.

Which code? In Chrome, the note goes one step further. It uses the Long Animation Frames API from Long Tasks and Long Animation Frames to find the longest script that ran during the click. It calls it longestScript:

The script behind the slow clickPop out in the code editor (opens in a new tab)JavaScript
// The longest script in that same click, from web-vitals' attribution (the page's origin left out).const script = { invoker: "BUTTON#cart.onclick", sourceFunctionName: "addToCart", sourceURL: "/cart.js?release=a", intersectingDuration: 529.9 }; console.log(`Started by: ${script.invoker}`);console.log(`Function: ${script.sourceFunctionName}() in ${script.sourceURL}`);console.log(`Time inside the click: ${script.intersectingDuration} ms`);
  • Line 2 is recorded, with the page's own address cut off the file name.
  • Line 4 prints Started by: BUTTON#cart.onclick. The invoker is what started the script: a click on the button.
  • Line 5 prints Function: addToCart() in /cart.js?release=a: the function, and the file it's in.
  • Line 6 prints Time inside the click: 529.9 ms. The README defines intersectingDuration as "The amount of time the longest script intersected the INP duration."

So 529.9 of the 533.7 ms of processing went on one function. Here it is, as release a shipped it:

cart.js, release aJavaScript
const count = document.querySelector("#count"); function addToCart() {  let total = 0;  for (let i = 0; i < 150000000; i++) total += i % 7;  count.textContent = Number(count.textContent) + 1;} document.querySelector("#cart").addEventListener("click", addToCart);
  • Line 9 runs addToCart on every click of the button.
  • Lines 4 and 5 recalculate the basket. The loop turns 150 million times. It stands in for real work, like working out totals.
  • Line 6 adds one to the count, the only thing the shopper sees change.
  • The browser can't paint the new count until the function ends. So the count waits for the whole loop.

Firefox and Safari don't have these script timings, so their notes can't name the function. For their visitors, a trace in the lab is how you find it.

What each part points at

Here the clues led to an API call and a loop. On another page, a different part could be the biggest. Read the table across: the biggest part, what it means, and where web.dev's guides to LCP and INP say to look.

What the biggest part tells you
If the biggest part isIt meansLook at
timeToFirstByteThe page's first byte came late.The server, and the road to it.
resourceLoadDelayThe image started loading late.How the browser finds the image. Is it in the HTML?
resourceLoadDurationThe image took long to download.The image's size, and how far it has to travel.
elementRenderDelayThe image arrived, but wasn't drawn yet.Stylesheets and scripts that hold up drawing.
inputDelayThe page was busy when the click came.Other tasks running at that moment.
processingDurationThe click's own code ran long.The event listeners. longestScript can name the slow one.
presentationDelayDrawing the next frame took long.Rendering work, such as a page with a great many elements.

Reproducing a field problem in the lab

The notes say what to look at. But they come from one moment on someone else's phone. You can't pause that moment, record it, or try a change on it. For that, you need the problem on your own computer.

To reproduce a problem means to make it happen again, on purpose, where you can watch it. web.dev's guide to slow clicks in the lab puts it plainly: "it's only when you're able to reproduce a slow interaction that you can fix it."

Its field guide adds that field data is what "real users experience", but "lab tools are often better at helping you identify and fix issues."

So the team tries. They load release a five times on a desktop, on a fast network, with nothing slowed down. Here is how that compares with the field's slowest segment:

The slow segment against a desktop in the labPop out in the code editor (opens in a new tab)JavaScript
// p75 from the field's phones on the slow network, and from 5 lab runs on a desktop with nothing slowed down, release a, in ms.const field = { LCP: 2608, INP: 576 };const lab = { LCP: 1056, INP: 168 }; for (const metric of ["LCP", "INP"]) {  console.log(`${metric}: field ${field[metric]} ms, lab ${lab[metric]} ms, ${(field[metric] / lab[metric]).toFixed(1)} times slower in the field`);}
  • Line 2 holds the field's p75 for phones on the slow network. Line 3 holds the p75 of the five lab runs.
  • Line 6 divides one by the other, for each metric.
  • It prints LCP: field 2608 ms, lab 1056 ms, 2.5 times slower in the field and INP: field 576 ms, lab 168 ms, 3.4 times slower in the field.

In the lab, the click took 168 ms. That's good: under 200. The problem the phones had wasn't there at all. web.dev's lab guide gives one important reason: you may be testing on a fast device and a fast connection, but your users may not be.

Now look at the parts, not the total. The lab's LCP of 1,056 ms looked fine. But its note showed the same cause as the field: 1,039.4 ms of it was resource load delay. The photo waited for the API here too. The fast network only made everything else quick.

Why it matters: a good number in the lab doesn't clear the page, so read its notes anyway. And when the lab hides a problem the field shows, make the lab more like the field.

Matching lab conditions to the slow segment

What was different about the slow segment? Not the page. It was the visitor's phone and network.

Real-life analogyA mechanic asks when the noise happens

Your car rattles, but only some mornings. The mechanic drives it round the block: no rattle. So he asks: "When exactly?" You think. "When the engine is cold, going up the hill." The next morning, he takes the cold car up the hill, and there it is. Now he can find it.

In real life: A drive round the block, engine warm
In the lab: A desktop on a fast network, nothing slowed down
In real life: "Cold engine, going up the hill"
In the lab: The slow segment: a phone's CPU and a slow network
In real life: Driving the cold car up the hill
In the lab: The lab, slowed down like the segment
In real life: The rattle comes back
In the lab: The lab's p75 matches the field's

Slowing a computer down on purpose is called throttling. The lab has a dial for each difference:

  • The CPU, the computer's processor. DevTools can make it run several times slower, as The Performance panel showed.
  • The network. DevTools can add delay, called latency, and limit how many bytes arrive each second.

Which number goes on the CPU dial? Chrome's DevTools docs warn: "Throttling is relative to your computer's capabilities." A 4x slowdown on a fast laptop and on a slow one end up at different speeds. They add: "DevTools can't truly simulate the CPUs of mobile devices." So don't guess. Try a few, and keep the one that matches the field.

Turning the CPU dial

The slow part of the click was JavaScript, and JavaScript runs on the CPU. So the course turned the CPU dial first. It loaded the page on a phone-sized screen at four slowdowns, five runs each:

Which CPU slowdown matches the phones?Pop out in the code editor (opens in a new tab)JavaScript
// p75 INP from 5 lab runs at each CPU slowdown on a phone screen, and from the field's 12 phone visits, release a, in ms.const lab = { "1x": 160, "2x": 304, "4x": 584, "6x": 856 };const field = 576; const [rate, inp] = Object.entries(lab).sort((a, b) => Math.abs(a[1] - field) - Math.abs(b[1] - field))[0];console.log(`closest: ${rate}, lab ${inp} ms, field ${field} ms`);
  • Line 2 holds the lab's p75 INP at each slowdown. 1x means not slowed at all.
  • Line 3 holds the p75 of the field's 12 phone visits, from both networks. The network barely touched the click: the phones' p75 INP was 584 ms on the fast one and 576 on the slow one.
  • Line 5 sorts the slowdowns by how far each one is from the field, and keeps the closest.
  • Line 6 prints closest: 4x, lab 584 ms, field 576 ms.
p75 INP, release a: Lab, CPU 1x 160 ms, Lab, CPU 2x 304 ms, Lab, CPU 4x 584 ms, Lab, CPU 6x 856 ms, Field phones 576 ms.Lab, CPU 1x160 msLab, CPU 2x304 msLab, CPU 4x584 msLab, CPU 6x856 msField phones576 ms
The lab's p75 INP at each CPU slowdown, and the field's phones (shaded). The lab at 4x lands closest to the field.

One thing made this example easy. The course's "phones" were this same computer, with Chrome slowing its CPU 4 times. So 4x had to win. With real phones, you don't know the number in advance. Trying a few is how you find it.

DevTools can help with the guess. The Performance panel lesson showed it measuring your computer to work out a slowdown for a low-tier or a mid-tier phone. The docs add that the panel "may also recommend a default throttling preset or a preset that approximates the experience of your users based on field data." Choosing test conditions covers picking settings from your own field data.

Adding the network

Now the network dial. The slow segment's network had 300 ms of latency and 200,000 bytes a second. The course set the lab to the same. It kept the phone screen and the 4x CPU, and ran release a five more times:

The matched lab against the slow segmentPop out in the code editor (opens in a new tab)JavaScript
// p75 from 5 lab runs (phone screen, CPU 4x, slow network) and from the field's phones on the slow network, release a, in ms.const lab = { LCP: 2600, INP: 576 };const field = { LCP: 2608, INP: 576 }; for (const metric of ["LCP", "INP"]) {  console.log(`${metric}: lab ${lab[metric]} ms, field ${field[metric]} ms, ${Math.abs(field[metric] - lab[metric])} ms apart`);}
  • Line 2 holds the matched lab's p75. Line 3 holds the field's, for phones on the slow network.
  • Line 6 prints how far apart they are: LCP: lab 2600 ms, field 2608 ms, 8 ms apart and INP: lab 576 ms, field 576 ms, 0 ms apart.

The rattle is back: the lab now behaves like the slow segment. Why it matters: any change can be tried here in minutes, before a single user sees it.

It's also the moment to record a trace. The Performance panel lesson showed how to record a click, and Reading a performance trace how to follow a long task to the function inside it. web.dev says the Interactions track "helps you line up the interaction and the activity that occurred in the main thread track beneath it." Below the click, you'd look for the task that ran addToCart.

Fixing the cause

The clues named two causes: a photo that waited for an API, and a click that waited for a loop. Fix those two things, not the numbers around them.

Put the photo in the HTML

Release a was like a recipe that says "phone the supplier to ask which tea to use": nothing can start until they answer. web.dev's LCP guide describes the same problem. When a script has to run before the image is found, that "usually involves waiting for network requests to finish". Its verdict: "This is never optimal."

Its advice: "your LCP resource should be discoverable from the HTML source." Release b does just that:

The photo, release bPop out in the code editor (opens in a new tab)HTML
<img id="hero" src="/hero.jpg" alt="Mint tea" width="412" height="275" fetchpriority="high">
  • Line 1 is the whole change. The src is right there in the HTML, so the browser finds it while it reads the page, with no API call first.
  • fetchpriority="high" is a hint about how much this image matters. web.dev explains that browsers don't load images at the highest priority at first, "as they are not render-blocking resources."
  • It suggests the hint "if you think it's likely to be your page's LCP element." Here, it always was.

Load the LCP resource early goes further, with images that only a stylesheet or a script can name.

Show the count first, recalculate later

Now the click. The count waited for the loop. But the shopper only needs to see the count change. The rest can happen a moment later.

Real-life analogyServe the tea, then write in the stock book

At Leaf and Kettle, every sale also goes in the stock book. A new waiter writes the whole entry before he hands over the tea, so the customer stands at the counter waiting. The manager tells him: hand over the tea first, then write in the book. The book still gets written. The customer just doesn't wait for it.

In real life: Handing over the tea
In JavaScript: Changing the count, which the next frame needs
In real life: Writing in the stock book
In JavaScript: recalculate(): the loop
In real life: Writing it after the tea is served
In JavaScript: Running it after the next frame
In real life: How long the customer stood waiting
In JavaScript: INP

web.dev's INP guide gives the same advice: "limit what gets run to just the logic required to apply visual updates for the next frame. Everything else can be deferred to a subsequent task." A task is one piece of work the browser's main thread runs from start to finish. Here is release b's cart code:

cart.js, release bJavaScript
const count = document.querySelector("#count"); function addToCart() {  count.textContent = Number(count.textContent) + 1;  requestAnimationFrame(() => setTimeout(recalculate, 0));} function recalculate() {  let total = 0;  for (let i = 0; i < 150000000; i++) total += i % 7;} document.querySelector("#cart").addEventListener("click", addToCart);
  • Line 4 adds one to the count first. That's the visible update.
  • Line 5 puts the rest off, in two steps. requestAnimationFrame() asks the browser to call a function "before the next repaint".
  • Inside it, setTimeout(recalculate, 0) puts recalculate in a new task. web.dev's example uses the same pair to defer the work "until at least the next frame".
  • Lines 8 to 11 are the same loop as before, moved into its own function. It still runs. The shopper just doesn't wait for it.

The pair in line 5 looks odd. web.dev calls it "an effective method that works in all browsers to prevent non-critical code from blocking the next frame." Here is the click, before and after:

The p75 click of phones on the slow network. Release a, INP 576 ms: Click, then Recalculate, then Count + 1, then Paint. Processing: 533.7 ms, then the paint. Release b, INP 48 ms: Click, then Count + 1, then Paint, then Recalculate. Processing: 3.3 ms. The loop waits for the paint.Release a: INP 576 msClickRecalculateCount + 1PaintProcessing: 533.7 ms, then the paint.Release b: INP 48 msClickCount + 1PaintRecalculateProcessing: 3.3 ms. The loop waits for the paint.
The p75 click of phones on the slow network. In release a, the recalculation runs before the count changes, so the paint waits for it. In release b, the count changes first, and the recalculation runs after the paint.

Both fixes are in. Before shipping, try them where the problem showed up: the matched lab. Five runs of release b gave a p75 LCP of 1,268 ms and an INP of 56 ms, down from 2,600 and 576.

That's the lab. It proves the fix works on one phone setup, on one network. Real users have every other phone and network. They decide.

Confirming the fix in the field

Release b goes live, and new beacons arrive. Why check them, when the lab already looked good? Think of the doctor again.

Real-life analogyA check-up a week after the treatment

After the treatment, your knee did well in the clinic. But you came in about the stairs at home. So a week later, the doctor asks: "How are the stairs at home now?" It's the same question as the first time, so the two answers compare. One bad day doesn't worry her. A bad week would.

In real life: The good test in the clinic
In field data: The matched lab, after the fix
In real life: "How are the stairs at home now?"
In field data: Field data from the new release
In real life: The same question as the first time
In field data: The same segments as before
In real life: One bad day in a good week
In field data: One slow visit in a good p75

To compare before and after, every beacon has to say which release sent it. The shop's beacon carried a segment object with the release in it, next to the device and the network. With that, you can split the field by release, then by segment:

Each segment, before and afterPop out in the code editor (opens in a new tab)JavaScript
// p75 from 6 field visits in each segment, before (a) and after (b) the fix, in ms.const p75 = {  "desktop, fast": { a: { LCP: 1064, INP: 168 }, b: { LCP: 56, INP: 48 } },  "desktop, slow": { a: { LCP: 2588, INP: 168 }, b: { LCP: 1252, INP: 56 } },  "phone, fast": { a: { LCP: 1088, INP: 584 }, b: { LCP: 120, INP: 56 } },  "phone, slow": { a: { LCP: 2608, INP: 576 }, b: { LCP: 1268, INP: 48 } },}; for (const [segment, { a, b }] of Object.entries(p75)) {  console.log(`${segment}: LCP ${a.LCP} to ${b.LCP} ms, INP ${a.INP} to ${b.INP} ms`);}
  • Lines 3 to 6 hold each segment's p75 for release a and release b, from 6 field visits each.
  • Line 10 prints one line for each segment. The desktops: desktop, fast: LCP 1064 to 56 ms, INP 168 to 48 ms and desktop, slow: LCP 2588 to 1252 ms, INP 168 to 56 ms.
  • The phones: phone, fast: LCP 1088 to 120 ms, INP 584 to 56 ms and phone, slow: LCP 2608 to 1268 ms, INP 576 to 48 ms.

Every segment got faster, in both metrics:

  • On the fast network, LCP fell from over 1,000 ms to 120 ms or less. The photo no longer waits for the API.
  • On the slow network, LCP fell by more than half. That's from 2,588 to 1,252 ms on desktops, and 2,608 to 1,268 on phones.
  • On the phones, INP fell from 584 and 576 ms to 56 and 48. The click no longer waits for the loop.

Why segment by segment? Suppose release b just happened to get more desktop visits. Its total would improve with no fix at all. That's the quieter month at the café from Segmenting field data. Comparing each segment with itself rules it out.

One slow visit

One visit didn't fit. A desktop on the slow network had an INP of 816 ms after the fix. Its note showed 814.7 ms of presentation delay and 1.1 ms of processing. Its longestScript was empty, so the note names no script to blame.

Presentation delay is the wait from the end of the click's code until the next frame is on screen. The recording can't tell what held that frame up.

It's one visit in six, though. The segment's six INPs were 48, 48, 48, 48, 56 and 816 ms. The p75 is the 5th of 6: 56 ms. Like the 20-minute wait on the tally sheet, one slow visit can move the p75 by one place at most. Why it matters: one bad visit isn't a failed fix. A p75 that climbs back up would be.

What's left

Read the note again, too. Here are LCP's four parts for the slow segment's p75 visit, before and after:

LCP subparts of the p75 phone visit on the slow network. Before the fix, LCP 2,608 ms: Time to first byte 2.7 ms, Resource load delay 1,760.9 ms, Resource load duration 821.1 ms, Element render delay 23.3 ms. After, LCP 1,268 ms: Time to first byte 1.6 ms, Resource load delay 339.7 ms, Resource load duration 913.6 ms, Element render delay 13.1 ms.Time to first bytea: 2.7 msb: 1.6 msResource load delaya: 1,760.9 msb: 339.7 msResource load durationa: 821.1 msb: 913.6 msElement render delaya: 23.3 msb: 13.1 ms
LCP's four parts for the p75 visit of phones on the slow network. Plain: release a. Shaded: release b. The delay shrank the most, and the photo's own download is now the biggest part.
  • After the fix, the HTML's first byte arrived at 1.6 ms, and its last byte at 325.2 ms. As before, the slow network spent its time inside the HTML's download.
  • The photo started at 337.8 ms, 12.6 ms after the HTML's last byte. It no longer waits for an API.
  • The biggest part now is the photo's own download: 913.6 ms. That's the next clue to follow.

One last caution. Each segment here had only 6 visits, so only big changes stand out this clearly. Smaller changes need more visits, and some statistics. That's the next lesson.

Which step is it?

Sort each task into its step. Ask yourself: am I reading the notes, making it happen in the lab, changing the code, or checking real users?

Which step is it?
  • Find which element slow phone visits clicked
  • Find the biggest LCP part of the p75 visit
  • Slow the lab's CPU until its INP matches the phones
  • Load the page on the slow segment's network
  • Show the count first, then recalculate after the next frame
  • Put the photo's <img> in the HTML
  • Compare each segment's p75 before and after
  • Tag every beacon with its release
Try it yourself
0 of 8 correct

Sort each task by its step.

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

Common mix-ups

  • "The lab says it's fast, so the field is wrong." No. The lab was a desktop on a fast network. It gave an INP of 168 ms, while the field's phones got 576. Set up like the slow segment, the lab agreed with the field.
  • "A slow LCP means a big image." No. Here 68% of LCP went by before the photo even started. A smaller photo would only have touched the 31% spent downloading it.
  • "Any CPU slowdown will do." No. 2x gave 304 ms and 6x gave 856 ms. Only 4x matched the field's phones, and only on this computer.
  • "A good result in the lab proves the fix." No. It proves it for one setup. Real users bring every other phone and network, so check the field.
  • "A better total proves it." No. If the mix of visitors changes, the total moves on its own. Compare each segment with itself.
  • "One slow visit after the fix means it failed." No. One desktop visit had an 816 ms INP, and its note named no script. Its segment's p75 stayed at 56 ms.

Practice exercises

Answer first, then use the hints.

Exercise 1 · Warm-upThe delay after the fix

This is the same segment's p75 visit, after the fix. What does the resourceLoadDelay line print now?

Starter codePop out in the code editor (opens in a new tab)JavaScript
// web-vitals' LCP attribution for the p75 visit of phones on the slow network, release b, in ms.
const lcp = { target: "#hero", timeToFirstByte: 1.6, resourceLoadDelay: 339.7, resourceLoadDuration: 913.6, elementRenderDelay: 13.1 };

const { target, ...subparts } = lcp;
const total = Object.values(subparts).reduce((sum, ms) => sum + ms, 0);
console.log(`${target}: LCP ${Math.round(total)} ms`);
for (const [name, ms] of Object.entries(subparts)) console.log(`${name}: ${Math.round((ms / total) * 100)}%`);

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

    Exercise 2 · Warm-upA site with faster phones

    Another site's phones have a p75 INP of 300 ms. What does the example print now?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // p75 INP from 5 lab runs at each CPU slowdown on a phone screen, and a field whose phones measured 300 ms, release a, in ms.
    const lab = { "1x": 160, "2x": 304, "4x": 584, "6x": 856 };
    const field = 300;
    
    const [rate, inp] = Object.entries(lab).sort((a, b) => Math.abs(a[1] - field) - Math.abs(b[1] - field))[0];
    console.log(`closest: ${rate}, lab ${inp} ms, field ${field} ms`);

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

      Exercise 3 · PracticeThe CPU and the photo

      In the lab, at a 6x CPU slowdown, what was the p75 LCP, in ms?

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

        Exercise 4 · PracticeWhat's left on a fast phone

        After the fix, which part of LCP was the biggest for phones on the fast network?

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

          Exercise 5 · ChallengeA slowdown for your own computer

          In the Performance panel, which option under Capture settings > CPU makes slowdown presets that match low-tier and mid-tier phones?

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

            Exercise 6 · ChallengeWhen should the photo start?

            web.dev's LCP guide gives a rule of its own: the LCP resource should start loading at the same time as what?

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

              Check your understanding

              For each question, ask which step you're in: reading the notes, reproducing in the lab, fixing, or confirming.

              From field data to a fix quiz · 7 questionsScore: first tries count
              1. Question 1 of 7In the lab run with nothing slowed down, what does the resourceLoadDelay line print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // web-vitals' LCP attribution for the p75 of 5 lab runs on a desktop with nothing slowed down, release a, in ms.
                const lcp = { target: "#hero", timeToFirstByte: 1.1, resourceLoadDelay: 1039.4, resourceLoadDuration: 0.5, elementRenderDelay: 15 };
                
                const { target, ...subparts } = lcp;
                const total = Object.values(subparts).reduce((sum, ms) => sum + ms, 0);
                console.log(`${target}: LCP ${Math.round(total)} ms`);
                for (const [name, ms] of Object.entries(subparts)) console.log(`${name}: ${Math.round((ms / total) * 100)}%`);

                Choose an answer to see the explanation.

              2. Question 2 of 7What does the CPU sweep example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // p75 INP from 5 lab runs at each CPU slowdown on a phone screen, and from the field's 12 phone visits, release a, in ms.
                const lab = { "1x": 160, "2x": 304, "4x": 584, "6x": 856 };
                const field = 576;
                
                const [rate, inp] = Object.entries(lab).sort((a, b) => Math.abs(a[1] - field) - Math.abs(b[1] - field))[0];
                console.log(`closest: ${rate}, lab ${inp} ms, field ${field} ms`);

                Choose an answer to see the explanation.

              3. Question 3 of 7The field says phones have a poor INP, but your laptop measures 168 ms. What do you do first?

                Choose an answer to see the explanation.

              4. Question 4 of 7Resource load delay is most of a slow LCP. What does that point to?

                Choose an answer to see the explanation.

              5. Question 5 of 7INP's longest script is addToCart, and it ran during processing. Which fix fits?

                Choose an answer to see the explanation.

              6. Question 6 of 7The fix is live. How do you confirm it?

                Choose an answer to see the explanation.

              7. Question 7 of 7After the fix, one visit in a segment of six has an INP of 816 ms. The segment's p75 is 56 ms. What does that tell you?

                Choose an answer to see the explanation.

              Key takeaways

              • Start from the field: find the slow segment, and read the visit at its p75.
              • Attribution names the element and the biggest part of the metric. In Chrome, it often names the function too.
              • Reproduce with the slow segment's CPU and network. Try a few CPU slowdowns, and keep the one closest to the field.
              • Fix the cause the clues named, and check it in the same matched lab.
              • Confirm with the same segments in the field, with every beacon tagged by its release.

              Remember the one-liner.
              The field says where it hurts, the lab shows why, and the field again says when it's better.

              Sources

              The facts on this page come from these docs:

              Coming next: Reading performance data. Six visits a segment made the changes here easy to see. Next come histograms, percentiles, sample sizes and confidence, so you can tell a real change from noise.

              CompleteFrontend Clear concepts. Working examples.