completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

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.

By the end, you can
  • 01
    Find the slow tap in field dataSend web-vitals' attribution with each visit's INP, then group the reports by element and by load state.
  • 02
    Make it happen on your own computerSlow the CPU down to match your visitors, or tap while the page is still loading.
  • 03
    Read 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:

The field, and Ravi's own tapPop out in the code editor (opens in a new tab)JavaScript
// 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, good and Ravi'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.

Two rows. Laptops: field p75 96 ms, good. Ravi's tap, full speed: real frames of the tea list at the tap on Sort by price, and the sorted list at the next frame, 80 ms later. Phones: field p75 258 ms, needs improvement. Asha's tap, CPU 4x slower: real frames of the tea list at the tap on Sort by price, and the sorted list at the next frame, 272 ms later.at the tapnext frameLaptopsRavi's tap, full speedfield p75: 96 msLaptops: the tea list in its first order, at the tap80 msuntil it changedLaptops: the tea list sorted by price, at the next framePhonesAsha's tap, CPU 4x slowerfield p75: 258 msPhones: the tea list in its first order, at the tap272 msuntil it changedPhones: the tea list sorted by price, at the next frame
Real frames from the course's recording. Chrome timed the same tap at 80 ms at full speed, and 272 ms with the CPU slowed 4 times. The cards show each group's p75 in the field.

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.

Real-life analogyA smoke alarm doesn't say which room

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
The main idea

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:

  1. Find the slow tap in reports from real visits.
  2. Make it happen on your own computer: on a slower CPU, or while the page loads.
  3. 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:

Sending INP with its attribution, shortenedJavaScript
// 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: pointer for clicks and taps, keyboard for key presses.
  • loadState: how far the page had loaded when the tap came. The last section explains its four values.
  • inputDelay, processingDuration and presentationDelay: 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 fieldPop out in the code editor (opens in a new tab)JavaScript
// 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, complete and 384 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.

Real-life analogyLate buses, counted by route and by hour

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:

Group reports by elementPop out in the code editor (opens in a new tab)JavaScript
// 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 ms and #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:

The 47 field reports grouped two ways, with the p75 INP of each group. By element: #sort, 14 reports, p75 384 ms; (no element), 3 reports, p75 96 ms; #search, 8 reports, p75 72 ms; #teas>li>button.add, 22 reports, p75 56 ms. By load state: dom-interactive, 7 reports, p75 355 ms; complete, 40 reports, p75 256 ms.By element#sort14 reports384 ms(no element)3 reports96 ms#search8 reports72 ms#teas>li>button.add22 reports56 msBy load statedom-interactive7 reports355 mscomplete40 reports256 ms200 ms
Each bar is one group's p75, to scale. The dashed line is 200 ms, the limit for a good INP. Grouped by element, Sort by price is the slow one. Grouped by load state, the taps during loading are.

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.

Playground: group the field reports
By element47 reports
  • #sort14 reportsp75 384 ms, slowest 384 msneeds improvement; at the p75, mostly processing
  • (no element)3 reportsp75 96 ms, slowest 96 msgood; at the p75, mostly processing
  • #search8 reportsp75 72 ms, slowest 88 msgood; at the p75, mostly processing
  • #teas>li>button.add22 reportsp75 56 ms, slowest 520 msgood; at the p75, mostly processing
Try it yourself
Worst p75: #sort, 384 ms

Its report at the p75 was mostly processing. Rows are sorted by p75, worst first.

47 reports from the course's stand-in visits, sent by web-vitals 6.2.3's attribution build in Chrome 157. Laptops ran at full speed, phones with the CPU slowed 4 times, cheaper phones 6 times. The five taps that came while reviews.js ran are counted without DevTools' hold.

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:

shop.js: work() and the Sort handlerJavaScript
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 root count times. done keeps 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.

Real-life analogyDimming the lights to read the small print

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:

The same tap at three CPU speedsPop out in the code editor (opens in a new tab)JavaScript
// 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 ms and 6x: 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:

Playground: Sort by price at three CPU speeds
Full speed80 ms, 72 ms, 80 ms
  • input delay4.3 ms
  • processing71.2 ms
  • presentation delay4.5 ms

DevTools' Interactions track: Pointer, 80.6 ms, under 200 ms: no red triangle.

web-vitals' longest script: sortByPrice, during the processing-duration.

Try it yourself
80 ms: good

The middle of three taps. In the field, laptops reported 96 ms to 104 ms for Sort by price.

One tap on Sort by price, three times at each speed, in Chrome 157. The parts come from Chrome's entry for the click. DevTools' numbers come from its own trace engine.

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.

Real-life analogyTwo cameras on one clock

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:

The tap on Sort by price with the CPU slowed 4 times, to scale, as DevTools draws it. Interactions track: a Pointer interaction of 269.2 ms: input delay 4.8 ms, processing 258.8 ms, presentation delay 5.6 ms, over the 200 ms line. Main track under it: a task of 262.3 ms, inside it Event: click for 258.8 ms, and inside that sortByPrice for 258.1 ms.Interactions trackPointer, 269.2 msred triangleinput delay: 4.8 mspresentation delay: 5.6 msMain trackTaskEvent: clicksortByPricetap100 ms200 ms269.2 ms
The 4x tap, to scale, as DevTools' Interactions track draws it, with the Main track underneath. The thin whiskers are the two delays. Almost all of the tap is the solid bar, and right under it is sortByPrice.

Hover over the bar, and a tooltip gives its three parts. Here are the numbers DevTools shows for this tap:

The tooltip's three partsPop out in the code editor (opens in a new tab)JavaScript
// 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 ms and Over 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, except async ones, 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.

Real-life analogyA café that's still opening

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 a tap during loadingPop out in the code editor (opens in a new tab)JavaScript
// 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.jsJavaScript
// 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 taps Add on Mint tea at 1,079.6 ms, while reviews.js's task runs from 1,039.7 ms for 356.3 ms, until 1,396 ms. Real frames: the list with Cart: 0 at the tap, and the next frame, with reviews.js's line and Cart: 1. The tap's handler, addToCart, starts just after the task ends: an input delay of 317.8 ms. It runs for 25.2 ms, and the next frame comes 4.8 ms later, 347.8 ms after the tap.the tap: 1,079.6 msthe tea list with Cart: 0, as Asha taps Addon screen since 107.9 msnext framethe line from reviews.js has appeared, and the cart says 1reviews.js's line, and Cart: 1347.8 msuntil it changedMain tracktapreviews.js's task: 356.3 msaddToCartinput delay: 317.8 msnext frame
Real frames and tasks from the recording, in ms since the page started. The tap waits for the rest of reviews.js's task. The screen stands still until the handler has run. Then reviews.js's line and the cart change in the same frame.

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:

Playground: tap Add during loading, or after
While reviews.js runs357.7 ms, 347.8 ms, 341.4 ms
  • input delay317.8 ms
  • processing25.2 ms
  • presentation delay4.8 ms
  • The HTML was read at 48.9 ms.
  • reviews.js ran from 1,039.8 ms for 350 ms.
  • Asha tapped at 1,079.6 ms. reviews.js's task ended at 1,396 ms, and her handler could start at 1,397.4 ms. DevTools held the course's tap 12.2 ms more.
  • DOMContentLoaded started at 1,389.8 ms, and the load ended at 1,394.7 ms.
Try it yourself
347.8 ms, loadState dom-interactive

Mostly input delay, by web-vitals' report. Its longest script: reviews.js, a classic-script, during the input-delay.

Taps on Add, Mint tea, on Asha's phone: Chrome 157 with the CPU slowed 4 times. The server held reviews.js back 1,000 ms, like a slow network. During loading, the waits are worked out from the trace's tasks, without DevTools' hold on the course's taps.

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.

CrUX, your own reports, and a lab trace
QuestionCrUXYour attribution reportsA 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:

Which step of the hunt is it?
  • 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
Try it yourself
0 of 8 correct

Sort each task into the step it belongs to.

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

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.

Exercise 1 · Warm-upGroup by load state

Six real reports, grouped by load state. What does the second line print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
// 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`);

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

    Exercise 2 · Warm-upWhich load state?

    This is the rule web-vitals uses, with plainer names. What does it print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // 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}.`);

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

      Exercise 3 · PracticeThe slowest key press

      In the field reports playground, group by kind of input. What is the slowest keyboard report, in ms?

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

        Exercise 4 · PracticeThe handler at 6x

        In the CPU playground, pick the 6x slowdown. How long did the handlers run in the middle tap, in ms?

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

          Exercise 5 · ChallengeWhat was running?

          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?

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

            Exercise 6 · ChallengeWhere dom-interactive ends

            In web-vitals' own code, which time from the navigation entry ends the dom-interactive state?

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

              Check your understanding

              For each question, ask where the answer lives: in the field, in the lab, or in a trace.

              The finding slow interactions quiz · 7 questionsScore: first tries count
              1. 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.

              2. Question 2 of 7This code groups four laptop reports by element. What does its first line print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // 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.

              3. 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.

              4. 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.

              5. Question 5 of 7What does DevTools' red triangle on an interaction mean?

                Choose an answer to see the explanation.

              6. 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.

              7. 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 predictPop out in the code editor (opens in a new tab)JavaScript
                // 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:

              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.

              CompleteFrontend Clear concepts. Working examples.