Finding slow interactions
Use web-vitals attribution from real visits to find which tap is slow and when, then reproduce it in DevTools on a slow CPU and during page load.
- 01Find the slow tap in field dataSend web-vitals' attribution with each visit's INP, then group the reports by element and by load state.
- 02Make it happen on your own computerSlow the CPU down to match your visitors, or tap while the page is still loading.
- 03Read the tap in a traceFind it on DevTools' Interactions track, read its three parts, and follow it down to the code that ran.
Which tap is slow, and for whom?
Ravi builds the Leaf and Kettle site. Every morning, he checks how real visits went. This morning, one number worries him: the tea list's INP. INP, or Interaction to Next Paint, is how long a page takes to answer a tap, a click or a key press. Lesson 26.1 showed how it works.
His numbers are field data: measurements from real visits, on real devices. They come as a p75, the 75th percentile. That's the value three in four visits were at or under. CrUX, Chrome's record of real visits from lesson 10.1, gives the same kind of number. It splits it into phones and desktops. Here are Ravi's numbers, and one tap he tried himself:
// INP at the 75th percentile, from the tea list's field data, and Ravi's own tap. Times in ms.const field = { phones: 258, laptops: 96 };const ravisTap = 80;const rate = (ms) => (ms > 500 ? "poor" : ms > 200 ? "needs improvement" : "good"); console.log(`Phones in the field: ${field.phones} ms, ${rate(field.phones)}`);console.log(`Laptops in the field: ${field.laptops} ms, ${rate(field.laptops)}`);console.log(`Ravi's tap on Sort by price: ${ravisTap} ms, ${rate(ravisTap)}`);- Line 1 is a comment. Times are in milliseconds (ms), thousandths of a second.
- Line 2 holds the field's p75 for phones and for laptops.
- Line 3 holds Ravi's own tap on Sort by price, on his laptop.
- Line 4 rates a time the way lesson 26.1 did: good up to 200 ms, poor over 500 ms.
- Lines 6 to 8 print
Phones in the field: 258 ms, needs improvement,Laptops in the field: 96 ms, goodandRavi's tap on Sort by price: 80 ms, good.
So phone visitors wait too long. But which tap is slow for them? On Ravi's laptop, every tap feels quick. And CrUX can't say. web.dev puts it plainly: "What CrUX can't do, however, is tell you what is causing problems." CrUX keeps one p75 for a page, not one for each tap.
Here is the same tap, Sort by price, on Ravi's laptop and on a phone like Asha's. These are real frames Chrome captured. A frame is one picture the browser draws on the screen.
The screens are the same. Only the wait is different: 80 ms on the laptop, 272 ms on the phone. Why it matters: a visitor can't tell you which tap was slow. And your own fast computer may never show it.
The smoke alarm in Meera's hallway goes off. It tells her there's smoke somewhere in the house. It doesn't say which room. So she walks from room to room until she finds the burnt toast. To be sure, she makes another slice and watches the toaster.
- In real life: The alarm in the hallway
- In web performance: Field data: INP needs improvement on phones
- In real life: Walking from room to room
- In web performance: Grouping each visit's report by element, and by when it came
- In real life: The burnt toast in the kitchen
- In web performance: The slow tap: Sort by price
- In real life: Making another slice and watching
- In web performance: Making the slow tap happen on your own computer
Field data says that INP is slow. To find which tap, send web-vitals' attribution with each visit's INP. Group the reports by element and by load state. Then make that tap happen on your own computer, and read it in DevTools. Find it in the field, make it happen in the lab, then read it in a trace.
The rest of the lesson takes those three steps:
- Find the slow tap in reports from real visits.
- Make it happen on your own computer: on a slower CPU, or while the page loads.
- Read it on DevTools' Interactions track.
How the course recorded it
- The stand-in field: a page on the course's own computer has no CrUX data. So the course made 48 visits to the tea list stand in for Ravi's visitors. 12 came from laptops at full speed. 24 came from phones like Asha's, and 12 from cheaper phones.
- The phones: this computer, in Chrome 157. Its processor, the CPU, was slowed 4 times for a phone and 6 times for a cheaper one. Lessons 22.1 and 26.2 used the same 4x stand-in for Asha's phone.
- What each visitor did: most waited for the page, then sorted by price, added teas, or searched. Some tapped Add before the page had finished loading.
- The reports: as each visitor left, the page sent its INP. It used the web-vitals library, a small script from Chrome's team, version 6.2.3. 47 of the 48 reports arrived. One laptop's report never came.
- The lab: the course tapped Sort by price three times at each of three CPU speeds. It also tapped Add three times while the page was loading, and three times after. Each time, Chrome wrote a trace: a detailed record of everything it did, with screenshots.
- The page: lesson 26.1's tea list, with new code. Its handlers do a fixed amount of work, and a script called reviews.js runs as the page starts. You'll see both below.
- The taps: the course tapped and clicked through Chrome's DevTools protocol, which lets a program drive the browser. DevTools passes clicks straight on. But it waits until the page has drawn a frame before it passes each tap on. A real finger's tap isn't held. It runs as soon as the work in front of it is done. Some taps came while reviews.js ran. For those, the lesson works out the wait from the recorded work. That takes 8.3 to 21.8 ms off each of them: five in the field, and three in the lab.
Finding the slow tap in field reports
The field data gives one number. Ravi needs one report for each visit, with clues in it. The web-vitals library can send that. Its attribution build adds an attribution object to each INP report: notes on where the time went. Lesson 26.2 read one. Here is how the tea list sends them:
// The tea list sends each visit's INP, with its attribution, as the page is hidden. Shortened.webVitals.onINP(({ value, attribution }) => { const { interactionTarget, interactionType, loadState } = attribution; const { inputDelay, processingDuration, presentationDelay } = attribution; navigator.sendBeacon("/collect", JSON.stringify({ value, interactionTarget, interactionType, loadState, inputDelay, processingDuration, presentationDelay, }));});- Line 1 is a comment. The page loads the attribution build with a script tag. That gives it a global object,
webVitals. - Line 2 asks for INP. web-vitals calls the function when the page is hidden, with the visit's INP in
value. - Lines 3 and 4 take six fields out of the attribution. The list below explains each one.
- Lines 5 to 8 send them to Ravi's server with
sendBeacon. It sends a small message that still gets out as the page closes, as lesson 12.3 showed.
interactionTarget: the element that was tapped, as a CSS selector. A selector is a short text that points at an element, like#sort.interactionType:pointerfor clicks and taps,keyboardfor key presses.loadState: how far the page had loaded when the tap came. The last section explains its four values.inputDelay,processingDurationandpresentationDelay: the three parts of the tap, from lesson 26.2. They are the wait before the handlers start, the handlers running, and drawing the next frame.
Here is one report that arrived, from a cheaper phone:
// One report from the field: a cheaper phone tapped Sort by price. Times in ms.const report = { value: 384, interactionTarget: "#sort", interactionType: "pointer", loadState: "complete", inputDelay: 5.7, processingDuration: 373, presentationDelay: 5.3,};const parts = report.inputDelay + report.processingDuration + report.presentationDelay;console.log(`${report.interactionTarget}, ${report.interactionType}, ${report.loadState}`);console.log(`${Math.round(parts)} ms, of which processing ${report.processingDuration} ms`);- Lines 2 to 5 hold the report, as Ravi's server got it.
- Line 6 adds up the three parts.
- Lines 7 and 8 print
#sort, pointer, completeand384 ms, of which processing 373 ms.
One report already says a lot. The tap was on #sort, the Sort by price button. It came after the page had loaded. Its event handlers, the code that runs for the tap, took 373 ms of the 384. But one report is one visit. Is it typical? To know, Ravi groups all of them.
A bus company hears that its buses run late. Every driver logs each trip, with its route and the hour it left. The manager sorts the logs by route. Route 7 is late most often. Then she sorts the same logs by hour. Before 9 in the morning, buses are late on every route.
- In real life: Each driver's log of a trip
- In web performance: Each visit's INP report
- In real life: Sorting the logs by route
- In web performance: Grouping the reports by element
- In real life: Sorting the same logs by hour
- In web performance: Grouping them by load state
- In real life: Route 7
- In web performance: #sort, the Sort by price button
- In real life: The morning rush
- In web performance: Taps while the page is still loading
Grouping takes a few lines. Here are nine real reports from phones:
// Nine reports from phones: [the element, INP in ms].const reports = [ ["#sort", 304], ["#teas>li>button.add", 32], ["#sort", 384], ["#search", 72], ["#sort", 256], ["#teas>li>button.add", 40], ["#teas>li>button.add", 330], ["#teas>li>button.add", 32], ["#search", 56],];const p75 = (values) => [...values].sort((a, b) => a - b)[Math.ceil(values.length * 0.75) - 1];const groups = {};for (const [element, ms] of reports) (groups[element] ??= []).push(ms);for (const [element, list] of Object.entries(groups)) { console.log(`${element}: ${list.length} reports, p75 ${p75(list)} ms`);}- Lines 2 to 5 hold nine reports: the element, and the INP.
- Line 6 finds the p75 the way lesson 12.4 did. It sorts the values, then takes the one three quarters of the way along.
- Lines 7 and 8 put each INP into its element's group.
- Lines 9 to 11 print one line for each group. The first is
#sort: 3 reports, p75 384 ms. Then come#teas>li>button.add: 4 reports, p75 40 msand#search: 2 reports, p75 72 ms.
#teas>li>button.add is how web-vitals names an Add button. It starts at the button and walks up the page. It stops at the first element with an id, the list #teas. So every Add button gets the same name. If you need to know which tea, web-vitals' generateTarget option lets you name elements your own way.
Here are all 47 reports, grouped two ways:
By element, Sort by price is the slow one. Its p75 is 384 ms, well over the 200 ms line. Its report at the p75 came after the page had loaded, and it was mostly processing. Why it matters: Ravi now knows which handler to read first, the one behind #sort.
The Add buttons look fine by element. Their p75 is 56 ms. But they also hold the slowest report of all: 520 ms, from a cheaper phone. Group by load state, and they show up. The 7 taps that came while the page was still loading have a p75 of 355 ms. Why it matters: a slow group can hide inside a fast one. Group your reports more than one way.
Three reports, all from laptops, came with no element. web-vitals' docs say this can happen. For each, the event web-vitals kept was a pointerdown, and Chrome's entry for it had no target. They were quick, so they don't change the answer.
Now try the other groupings: by device, and by kind of input.
#sort14 reportsp75384 ms, slowest384 msneeds improvement; at the p75, mostly processing(no element)3 reportsp7596 ms, slowest96 msgood; at the p75, mostly processing#search8 reportsp7572 ms, slowest88 msgood; at the p75, mostly processing#teas>li>button.add22 reportsp7556 ms, slowest520 msgood; at the p75, mostly processing
Its report at the p75 was mostly processing. Rows are sorted by p75, worst first.
Reproducing it on a slow CPU
Ravi knows which tap to fix: Sort by price. Next, he wants to reproduce it: make it happen again, on purpose, where he can watch it. Lesson 12.7 did this for a click. But on his laptop, Sort by price took 80 ms. Why didn't it happen there? The answer is in the handler:
let done = 0;// A stand-in for real work: the same number of steps on any computer.const work = (count) => { let sum = 0; for (let i = 0; i < count; i++) sum += Math.sqrt(i); done += sum; }; document.querySelector("#sort").addEventListener("click", function sortByPrice() { work(60_000_000); teas.append(...[...teas.children].sort((a, b) => a.dataset.price - b.dataset.price));});- Lines 1 to 3 make
work(), a stand-in for real code. It takes a square rootcounttimes.donekeeps the total, so the result is used. - Lines 5 to 8 are the Sort handler,
sortByPrice. Line 6 does 60 million steps of work. Line 7 moves the rows into price order.
Lessons 26.1 and 26.2 used busy(), which watched the clock. busy(300) took 300 ms on any computer. Real code isn't like that. It has a fixed number of steps to do, and a slower processor takes longer to do them. So this page counts steps instead.
Meera prints a new menu with small writing. In her bright office, she reads it easily. But customers sit in a dim café, and some of them squint. To see what they see, she turns the café lights down. Then she sits at a table and reads it again.
- In real life: Meera's bright office
- In DevTools: Ravi's fast laptop
- In real life: The dim café
- In DevTools: His visitors' slower phones
- In real life: Turning the café lights down
- In DevTools: CPU throttling in DevTools
- In real life: Squinting at the small print
- In DevTools: The slow tap on Sort by price
DevTools can turn the lights down. CPU throttling makes your computer's processor, the CPU, run several times slower. Lesson 7.1 showed where it is: in the Performance panel, open Capture settings, and pick a slowdown under CPU. The course tapped Sort by price three times at each of three speeds:
// One tap on Sort by price, three times at each CPU speed. INP in ms.const taps = { "1x": [80, 72, 80], "4x": [280, 272, 272], "6x": [400, 408, 408] };const middle = (list) => [...list].sort((a, b) => a - b)[1]; for (const [speed, list] of Object.entries(taps)) { console.log(`${speed}: ${middle(list)} ms`);}- Line 2 holds the three taps at each speed. 1x means full speed.
- Line 3 picks the middle of three.
- Lines 5 to 7 print
1x: 80 ms,4x: 272 msand6x: 408 ms.
At full speed, the tap is good. Slowed down, it needs improvement. Now compare it with the field. Phones reported 256 to 304 ms for Sort by price. Cheaper phones reported 384 ms, all four times. So 4x lands among the phones, and 6x near the cheaper phones. Why it matters: once the lab matches the field, a fix that helps here should help real visitors too.
Which slowdown to pick
DevTools' docs warn that "Throttling is relative to your computer's capabilities." 4x on a fast laptop and 4x on an old one end up at different speeds. They add that DevTools "can't truly simulate the CPUs of mobile devices".
Three things help:
- Calibrate. DevTools can time your computer and pick slowdowns for a low-tier and a mid-tier phone. Lesson 7.1 ran it on this computer: 2.96x for a mid-tier phone, and 11.28x for a low-tier one. The course's 4x and 6x sit between the two.
- Match the field. Try a few slowdowns, and keep the one whose taps look like your visitors' reports.
- Use a real phone. web.dev suggests remote debugging: DevTools on your computer, recording a real Android phone.
Pick a speed to see all three taps, the parts of the middle one, and what the field said:
DevTools' Interactions track: Pointer, 80.6 ms, under 200 ms: no red triangle.
web-vitals' longest script: sortByPrice, during the processing-duration.
The middle of three taps. In the field, laptops reported 96 ms to 104 ms for Sort by price.
Reading the Interactions track
The slow tap happens in the lab now. Next, Ravi wants to watch it. He opens the Performance panel and presses Record. He taps Sort by price, then stops. DevTools draws the recording as a trace, in rows called tracks.
Start with the Interactions track. Lesson 7.2 met it. It draws each tap, click or key press as one bar. DevTools names each bar by its kind: Pointer for clicks and taps, Keyboard for key presses.
Leaf and Kettle has two cameras. One watches the front door, the other the kitchen. Both show the same clock. A customer says she waited ages at 10:42. Meera finds 10:42 on the door camera. Then she plays the kitchen camera at the same time. There's the cook, on the phone.
- In real life: The camera on the front door
- In DevTools: The Interactions track: when each tap came, and how long it waited
- In real life: The camera in the kitchen
- In DevTools: The Main track: the work the main thread did
- In real life: The same clock on both
- In DevTools: One timeline for every track in the trace
- In real life: The kitchen at 10:42
- In DevTools: The Main track straight under the tap
Here is the course's 4x tap on Sort by price, as DevTools draws it:
sortByPrice.Hover over the bar, and a tooltip gives its three parts. Here are the numbers DevTools shows for this tap:
// The 4x tap on Sort by price, as DevTools' Interactions track shows it. Times in ms.const pointer = { inputDelay: 4.8, processing: 258.8, presentationDelay: 5.6 };const total = pointer.inputDelay + pointer.processing + pointer.presentationDelay; console.log(`Pointer: ${total.toFixed(1)} ms`);console.log(`Over the 200 ms line: ${(total - 200).toFixed(1)} ms`);- Line 2 holds the three parts DevTools shows.
- Line 3 adds them up.
- Lines 5 and 6 print
Pointer: 269.2 msandOver the 200 ms line: 69.2 ms.
Now read the picture from left to right:
- The thin line on the left is the input delay. The docs call it a whisker. Here it's 4.8 ms.
- The solid bar is processing: 258.8 ms.
- The whisker on the right is the presentation delay: 5.6 ms.
- The tap is over 200 ms, so DevTools puts a red triangle in its top right corner. web.dev adds that the part past 200 ms is striped. Here, that's 69.2 ms.
Then look straight down, like the kitchen camera. Under the bar is the Main track: everything the main thread did. That's the one thread that runs the page's JavaScript and draws its frames. First comes a task, one piece of work the main thread picks up. Inside it is Event: click. Inside that is a function call, and the name under it is sortByPrice. That's Ravi's handler, and it covers nearly the whole tap. Why it matters: the Interactions track tells you when to look. The Main track under it tells you what ran.
One tap fires several events, like pointerdown, pointerup and click. DevTools' source hides the ones nested inside the longest, so one tap shows as one bar.
DevTools' 269.2 ms and the page's own entry, 272 ms, are the same tap. An entry is Chrome's record of an event's times, from lesson 11.4. It rounds its duration to the nearest 8 ms. DevTools reads the trace's own start and end.
You don't always need a recording. The Performance panel's live metrics view lists every interaction as you go, with its three parts. web.dev suggests trying interactions there first. Then record a trace for the slow ones.
Taps while the page is still loading
Back to the second finding: the 7 taps during loading. web-vitals reports when each tap came as loadState. It has four values:
loading: the browser is still reading the HTML.dom-interactive: the HTML has been read, but scripts may still be loading or running.dom-content-loaded: the scripts have run, exceptasyncones, which run whenever they arrive.complete: everything has loaded, images and styles too.
web-vitals' docs warn that a tap while scripts run, usually in dom-interactive, can wait a long time. All 7 of the tea list's early taps were dom-interactive.
Leaf and Kettle opens at 8. At 7:55, the door is open and the menu is on the wall. A customer walks in and orders. But the staff are still unpacking the morning delivery. Her order waits until the last box is put away. Nothing about her tea was slow.
- In real life: The menu on the wall
- In web performance: The tea list, on screen and ready to tap
- In real life: Unpacking the morning delivery
- In web performance: reviews.js running as the page starts
- In real life: Waiting for the last box
- In web performance: The input delay
- In real life: "We're still opening"
- In web performance: loadState: dom-interactive
Here is one of those reports, from a phone:
// A report from the field: a phone tapped Add as the page loaded. Times in ms; the file name is shortened.const report = { value: 330, interactionTarget: "#teas>li>button.add", loadState: "dom-interactive", inputDelay: 301.8, processingDuration: 25.5, presentationDelay: 2.9, longestScript: { subpart: "input-delay", entry: { invokerType: "classic-script", sourceURL: "reviews.js" } },};const { subpart, entry } = report.longestScript;console.log(`Tapped while the page was ${report.loadState}: waited ${report.inputDelay} ms`);console.log(`Longest script: ${entry.sourceURL}, a ${entry.invokerType}, during the ${subpart}`);- Lines 2 to 6 hold the report. Line 5 is its
longestScript: the longest script running around the tap, which lesson 26.2 met. - Lines 3 and 4 leave out DevTools' hold. The report that arrived said 352 ms, with 323.6 ms of input delay. 21.8 ms of that was the hold. Without it, the tap waited until reviews.js's task ended.
- Line 8 prints
Tapped while the page was dom-interactive: waited 301.8 ms. - Line 9 prints
Longest script: reviews.js, a classic-script, during the input-delay.
classic-script means the browser was running a whole script file, not an event handler. web.dev says that long input delays with this kind of script mean your scripts take a long time to run as the page starts. Here is the script:
// reviews.js: works out this week's popular teas as the page starts.work(80_000_000);document.querySelector("#about").textContent = "Popular this week: Mint tea and Earl Grey.";- Line 1 is a comment.
- Line 2 does 80 million steps of work, with
work()from shop.js. - Line 3 writes the week's popular teas onto the page.
The page loads it with defer, so it runs after the HTML has been read and before DOMContentLoaded. On a slow network, it arrives late. The course's server held it back for 1 second, as a stand-in.
Tapping early in the lab
Ravi always waited for the page to load before he tapped. His visitors don't. web.dev's lab guide suggests slowing the network, then tapping as soon as the page paints. In DevTools, click Record and reload, and tap as soon as the list appears. The course tapped Add 40 ms after reviews.js arrived, on Asha's phone:
Asha tapped at 1,079.6 ms. reviews.js had started at 1,039.8 ms, and it ran for 350 ms. Its task then finished loading the page, and ended at 1,396 ms. Her tap had to wait for all of it. Then the browser spent 1.4 ms on the tap's first events, like pointerdown. So her click's handler started 317.8 ms after her tap: that's the input delay. It ran for 25.2 ms. The next frame came 4.8 ms later. So the whole tap took 347.8 ms. After the page had loaded, the same tap took 48 ms.
Chrome's own entry for this tap says 330 ms of input delay, and 360 ms in all. The extra 12.2 ms is DevTools' hold. When reviews.js's task ended, Chrome drew a frame with the cart still at 0. Only then did DevTools pass the tap on. A real finger doesn't wait for that frame. Its handler runs first. So reviews.js's line and Cart: 1 appear in the same frame. The course's trace in exercise 5 is the recording, so it still shows the held tap.
For 347.8 ms, Asha's screen stood still. Why it matters: a page that looks ready isn't always ready. If your visitors tap early, test early.
Not every early tap was slow. Two of the seven came while reviews.js was still downloading. The main thread was free, so they took 40 and 48 ms. Why it matters: dom-interactive is a clue, not a verdict. Read it with the input delay and the longest script.
Switch between the two taps to compare them:
- The HTML was read at
48.9 ms. - reviews.js ran from
1,039.8 msfor350 ms. - Asha tapped at
1,079.6 ms. reviews.js's task ended at1,396 ms, and her handler could start at1,397.4 ms. DevTools held the course's tap12.2 msmore. - DOMContentLoaded started at
1,389.8 ms, and the load ended at1,394.7 ms.
Mostly input delay, by web-vitals' report. Its longest script: reviews.js, a classic-script, during the input-delay.
How do you fix it? web.dev suggests splitting big scripts into smaller ones, loading code you don't need yet later, and removing code you don't use. Module 27 shows how to break a long task into short ones.
What each place can tell you
Read this table across. Each row is a question. Each column is a place to look for the answer.
| Question | CrUX | Your attribution reports | A lab trace |
|---|---|---|---|
| Is INP slow, and for which devices? | Yes. It gives one p75 for a page or a site, for phones and for desktops. | Yes. You can split your own visits any way you like. | Only for the one run you recorded. |
| Which element was tapped? | No. | Yes. interactionTarget is a selector for it. | Yes. You know, because you made the tap. |
| Was it while the page was loading? | No. | Yes. loadState says how far the load had got. | Yes, if you recorded the load and tapped during it. |
| Which part of the tap was long? | No. | Yes. The three parts come with every report. | Yes. The Interactions track shows them, and the tooltip gives the numbers. |
| Which code was running? | No. | Often. longestScript names the script, in Chrome only. | Yes. The Main track shows every task and function. |
Which step of the hunt is it?
Each task below is part of finding a slow tap. Sort it into its step:
- Group the INP reports by
interactionTarget - Split the reports by
loadState - Count how many slow reports came from phones
- Set the CPU to a 4x slowdown before tapping
- Calibrate CPU throttling for a mid-tier phone
- Tap the button as soon as the list appears
- Hover over the interaction to read its three parts
- Find the function call under Event: click
Sort each task into the step it belongs to.
Common mix-ups
- "CrUX can tell me which button is slow." No. It keeps one p75 for a page or a site, for phones and desktops. Your own reports say which element.
- "If a tap is quick on my laptop, it's quick for my visitors." No. Sort by price took 80 ms at full speed, and 272 ms with the CPU slowed 4 times.
- "A 4x slowdown turns my laptop into a phone." No. DevTools' docs say throttling depends on your own computer, and can't truly copy a phone's CPU. Calibrate, and check against the field.
- "Fix the element with the slowest report first." No. The slowest report, 520 ms, was on an Add button. But Add's p75 was 56 ms, and Sort by price's was 384 ms.
- "dom-interactive always means a slow tap." No. Two early taps took 40 and 48 ms. reviews.js was still downloading, so the main thread was free.
- "Only test after the page has loaded." No. Visitors tap as soon as they see a button. The early tap took 347.8 ms, and the same tap after loading took 48 ms.
- "DevTools and the page's own entry always agree." No. The 4x tap was 269.2 ms in DevTools and 272 ms in the entry, which rounds to 8 ms.
Practice exercises
Answer first, then use the hints.
Six real reports, grouped by load state. What does the second line print?
// Six reports: [the load state, INP in ms].
const reports = [
["complete", 256], ["dom-interactive", 330], ["complete", 32],
["dom-interactive", 40], ["complete", 384], ["dom-interactive", 520],
];
const p75 = (values) => [...values].sort((a, b) => a - b)[Math.ceil(values.length * 0.75) - 1];
const groups = {};
for (const [state, ms] of reports) (groups[state] ??= []).push(ms);
for (const [state, list] of Object.entries(groups)) console.log(`${state}: p75 ${p75(list)} ms`);dom-interactive: p75 520 ms. With only three values, the p75 is the slowest. Small groups are jumpy, so count your reports too.
This is the rule web-vitals uses, with plainer names. What does it print?
// One tap, and three times from the visit's navigation entry. Times in ms.
const tap = 525.4;
const page = { htmlRead: 36.2, scriptsDone: 1362.1, allLoaded: 1366.2 };
const state =
tap < page.htmlRead ? "loading" : tap < page.scriptsDone ? "dom-interactive" : tap < page.allLoaded ? "dom-content-loaded" : "complete";
console.log(`The tap came while the page was ${state}.`);The tap came while the page was dom-interactive. This phone tapped while reviews.js was still downloading. Its report said dom-interactive, and it took only 40 ms.
In the field reports playground, group by kind of input. What is the slowest keyboard report, in ms?
88 ms, from a cheaper phone typing in the search box. Even the slowest key press was good.
In the CPU playground, pick the 6x slowdown. How long did the handlers run in the middle tap, in ms?
391.8 ms. Almost the whole tap was Ravi's handler doing its 60 million steps on a slower CPU.
Open the course's trace of the early tap in Chrome's DevTools. A long task was running when the tap came. What does DevTools call the event inside it?
Evaluate script. DevTools shows the browser running a script file as Evaluate script. The Summary names the file: reviews.js.
In web-vitals' own code, which time from the navigation entry ends the dom-interactive state?
domContentLoadedEventStart. A tap before that time, and after domInteractive, is dom-interactive. If the time isn't set yet, it's dom-interactive too.
Check your understanding
For each question, ask where the answer lives: in the field, in the lab, or in a trace.
Question 1 of 7CrUX says the tea list's INP needs improvement on phones. What can't CrUX tell Ravi?
Choose an answer to see the explanation.
Question 2 of 7This code groups four laptop reports by element. What does its first line print?
Read the code, then predict// Four reports from laptops: [the element, INP in ms]. const reports = [["#sort", 104], ["#teas>li>button.add", 40], ["#search", 48], ["#sort", 96]]; const p75 = (values) => [...values].sort((a, b) => a - b)[Math.ceil(values.length * 0.75) - 1]; const groups = {}; for (const [element, ms] of reports) (groups[element] ??= []).push(ms); for (const [element, list] of Object.entries(groups)) { console.log(`${element}: ${list.length} reports, p75 ${p75(list)} ms`); }Choose an answer to see the explanation.
Question 3 of 7A tap is quick on Ravi's laptop, but phone visitors report it as slow. What should he try first?
Choose an answer to see the explanation.
Question 4 of 7Many slow reports say loadState dom-interactive, with a long input delay and a longest script of type classic-script. What's the likely cause?
Choose an answer to see the explanation.
Question 5 of 7What does DevTools' red triangle on an interaction mean?
Choose an answer to see the explanation.
Question 6 of 7DevTools and the page's own Event Timing entry give slightly different times for the same tap. Why?
Choose an answer to see the explanation.
Question 7 of 7This code finds the value that three in four of the field's taps during loading were at or under. What does it print?
Read the code, then predict// Seven reports from taps during loading: INP in ms. const duringLoad = [330, 258, 520, 40, 48, 177, 355]; const sorted = [...duringLoad].sort((a, b) => a - b); console.log(`p75: ${sorted[Math.ceil(sorted.length * 0.75) - 1]} ms`);Choose an answer to see the explanation.
Key takeaways
- Field data says whether INP is slow. web-vitals' attribution says which element, which kind of input, and when.
- Group the reports by element and by load state. A slow group can hide inside a fast one.
- If a slow tap is quick on your computer, slow the CPU down until the lab matches the field.
- The Interactions track shows each tap and its three parts. The Main track under it shows the code that ran.
- Visitors tap while pages load. Tap early in the lab too, and look for scripts running at that moment.
Remember the one-liner.
Let the field say which tap and when, make it happen in the lab, then read it on the Interactions track.
Sources
The facts on this page come from these docs and source files:
- Find slow interactions in the field, web.dev.
- CrUX and RUM differences, web.dev.
- CrUX methodology, Chrome for Developers.
- CrUX metrics, Chrome for Developers.
- content/browser/devtools/protocol/input_handler.cc, Chromium 157.0.8103.52, Chromium on GitHub.
- types/inp.ts, web-vitals 6.2.3, GoogleChrome on GitHub.
- attribution/onINP.ts, web-vitals 6.2.3, GoogleChrome on GitHub.
- lib/getSelector.ts, web-vitals 6.2.3, GoogleChrome on GitHub.
- lib/getLoadState.ts, web-vitals 6.2.3, GoogleChrome on GitHub.
- Manually diagnose slow interactions in the lab, web.dev.
- Performance features reference, Chrome for Developers.
- Throttling settings, Chrome for Developers.
- UserInteractionsHandler.ts, Chrome DevTools, ChromeDevTools on GitHub.
Coming next: Long tasks, the first lesson of Module 27. Any task over 50 ms can hold back a tap. It shows how to find them, and why they hurt.