completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

The three phases of an interaction

Split a slow tap into input delay, processing and presentation delay, see what makes each one long in Chrome, and learn which one to fix first.

By the end, you can
  • 01
    Split a slow interactionTurn an Event Timing entry, or web-vitals' attribution, into input delay, processing and presentation delay.
  • 02
    Name the usual cause of eachTell a busy main thread, a heavy listener and a big redraw apart, and see each in long animation frames or a trace.
  • 03
    Choose what to fix firstFind the biggest phase of the slowest interaction, and know which part of the course teaches its fix.

Where did the 304 ms go?

Asha is back on the Leaf and Kettle tea list. She taps Sort by price. For about a third of a second, nothing happens. Then the list jumps into its new order.

Lesson 26.1 timed that tap at 304 milliseconds (ms). It was the slowest moment of her visit. So it became her visit's INP, or Interaction to Next Paint. Good is 200 ms or less.

Ravi builds the shop's site. He wants that tap to be faster. But faster where? A tap isn't one job. It's a short chain of waits. If he speeds up the wrong one, nothing changes.

So his question is simple. Where did the 304 ms go?

Here is Asha's screen at four moments of that tap. These are real frames Chrome captured. A frame is one picture the browser draws on the screen.

Asha's screen in real frames at four moments of her tap on Sort by price. her tap, at 0 ms: the old order; handler starts, at 2.4 ms: the old order; handler ends, at 302.6 ms: the old order; next frame, at 304 ms: the list sorted by price. Between them: input delay 2.4 ms, processing 300.2 ms, presentation delay 1.4 ms.her tap0 msThe tea list at her tap: the old orderhandler starts2.4 msThe tea list at handler starts: the old orderhandler ends302.6 msThe tea list at handler ends: the old ordernext frame304 msThe tea list at next frame: the list sorted by priceinput delay2.4 msprocessing300.2 mspresentation delay1.4 ms
Real frames from lesson 26.1's recording, at the times in Chrome's entry for the tap. The screen changed only at the next frame. Chrome timed the whole tap at 304 ms.

The first three screens look the same. The list changed only at the last one. Between them are the three parts of the tap. Chrome wrote down the times that mark them. For each event, it keeps an entry: a small record of times, from lesson 11.4. Here are the times from the tap's entry, split with three subtractions:

Splitting Asha's slow tapPop out in the code editor (opens in a new tab)JavaScript
// Chrome's entry for Asha's tap on Sort by price, from lesson 26.1. Times in ms.const tap = { startTime: 14822.8, processingStart: 14825.2, processingEnd: 15125.4, duration: 304 }; console.log(`Input delay: ${(tap.processingStart - tap.startTime).toFixed(1)} ms`);console.log(`Processing: ${(tap.processingEnd - tap.processingStart).toFixed(1)} ms`);console.log(`Presentation delay: ${(tap.startTime + tap.duration - tap.processingEnd).toFixed(1)} ms`);
  • Line 1 is a comment. Times are in ms, counted from when the page started loading.
  • Line 2 holds four times from Chrome's entry for the tap's click event. startTime is the moment of the tap. processingStart is when the click's handlers were about to run. An event handler is the code that runs when an event happens. processingEnd is when the handlers finished. duration runs from the tap to the next frame.
  • Line 4 subtracts the tap from the start of the handlers. It prints Input delay: 2.4 ms.
  • Line 5 subtracts the handlers' start from their end. It prints Processing: 300.2 ms.
  • Line 6 takes what's left until the next frame. It prints Presentation delay: 1.4 ms.

So the answer is clear. Almost all of the 304 ms was processing. That was Ravi's own Sort handler, doing its 300 ms of work. Why it matters: Ravi now knows where to look. The fix is inside his handler.

These three parts have names. Lesson 11.4 met them as three waits at a café counter. This lesson goes further. For each part, it asks what makes it long, how to see it, and which to fix first.

  • Input delay: from the tap until the first handler starts.
  • Processing duration: while the handlers run. Lesson 11.4 called it processing.
  • Presentation delay: from the end of the handlers until the next frame is on the screen.

Add them up, and you get the whole tap: 2.4 + 300.2 + 1.4 = 304 ms.

Real-life analogyA late pizza has three suspects

You phone a pizza place. The pizza comes 45 minutes later. Whose fault was it? Maybe the phone rang and rang, because the cook was busy. Maybe your pizza took ages to make. Or maybe it sat by the oven until the driver came. A faster driver won't help if the phone rang for half an hour.

In real life: You phone the pizza place
In the browser: The tap: the entry's startTime
In real life: Someone picks up and starts your order
In the browser: The first handler starts: processingStart
In real life: Your pizza is ready
In the browser: The handlers end: processingEnd
In real life: The pizza at your door
In the browser: The next frame on the screen
In real life: Finding out which wait was long, before you complain
In the browser: Splitting the tap before you fix it
The main idea

Every click, tap or key press is three waits in a row: input delay, processing duration and presentation delay. Split a slow one, find its biggest part, and fix that part first. Each part has its own usual cause, so each points to a different fix.

Is it always the handler?

Asha's tap was slow in its handler. Does a slow tap always mean a slow handler? To find out, the course made copies of the tea list. In each copy, the Sort handler is quick. Then one other thing is made slow, in a different place each time.

Here is the quick handler every copy starts from:

The quick Sort handlerJavaScript
const byPrice = [...teas.children].sort((a, b) => a.dataset.price - b.dataset.price);document.querySelector("#sort").addEventListener("click", function sortByPrice() {  teas.append(...byPrice);});
  • Line 1 works out the order once, when the page starts. byPrice holds the rows, cheapest first.
  • Line 2 adds a click handler to the Sort by price button. The handler is named sortByPrice.
  • Line 3 is all the handler does. teas.append() moves each row to the end of the list, in that order. So the list ends up sorted.

Then the course made three copies, each slow in one place:

  • A busy page: a second script, analytics.js, keeps notes on what visitors do. It packs up its notes every 5 seconds. Packing takes 300 ms. Asha taps 60 ms after it starts.
  • An extra listener: this time, analytics.js notes every click on the page. Noting each click takes 300 ms.
  • A long list: the list holds 3,000 rows instead of 8. The same eight teas sit at the top. Below them are 2,992 house blends, all dearer.

The 300 ms of work is a stand-in. It's a loop that keeps the main thread busy, the way slow code would. The main thread is the one thread that runs a page's JavaScript and draws its frames.

How the course recorded it

  • The browser: Chrome 157, with the CPU slowed 4 times, as a stand-in for a slower phone. DevTools offers the same 4x slowdown. Asha's window was 412 by 480 pixels, with a touch screen, as in lesson 26.1.
  • The tap: Playwright, a tool that drives a real browser, tapped Sort by price once on each copy. It used Chrome's own touch input, through Chrome's DevTools protocol. DevTools waits until the page has drawn a frame before it passes a tap on. A real finger's tap isn't held. So on the busy page, the wait is worked out from the recorded work. That takes 5.3 ms off.
  • What the page measured: every event entry of 16 ms or more, and every long animation frame (lesson 11.5). It also ran the web-vitals library's attribution build, version 6.2.3: the version that also points at the cause. Chrome wrote a trace too: a detailed record of everything it did, with screenshots.

Here is each copy's tap, split the same way:

Four copies, four tapsPop out in the code editor (opens in a new tab)JavaScript
// Each copy's tap, from Chrome's entries: [input delay, processing, presentation delay] in ms.const taps = {  "The quick version": [9.3, 0.3, 6.4],  "A busy page": [251.2, 0.8, 6.7],  "An extra listener": [11.4, 301.6, 7],  "A long list": [14.1, 60.4, 293.5],};const names = ["input delay", "processing", "presentation delay"];for (const [copy, parts] of Object.entries(taps)) {  const total = parts.reduce((sum, part) => sum + part, 0);  const biggest = names[parts.indexOf(Math.max(...parts))];  console.log(`${copy}: ${Math.round(total)} ms, mostly ${biggest}`);}
  • Lines 2 to 7 hold the three parts of each tap, from Chrome's click entries. The busy page's input delay leaves out DevTools' hold.
  • Line 8 names the parts, in order.
  • Line 10 adds up the parts. Line 11 finds the biggest one.
  • Line 12 prints one line per copy. The first two are The quick version: 16 ms, mostly input delay and A busy page: 259 ms, mostly input delay. The last two are An extra listener: 320 ms, mostly processing and A long list: 368 ms, mostly presentation delay.

Three slow taps, each slow in a different part. And what did Asha see? The same thing every time:

Four copies of the tea list, one tap on Sort by price each, with the CPU slowed 4 times. On every copy, Asha's screen showed the old order at the tap and the sorted list at the next frame. The quick version: 16 ms, input delay 9.3 ms, processing 0.3 ms, presentation delay 6.4 ms. A busy page: 258.7 ms, input delay 251.2 ms, processing 0.8 ms, presentation delay 6.7 ms. An extra listener: 320 ms, input delay 11.4 ms, processing 301.6 ms, presentation delay 7 ms. A long list: 368 ms, input delay 14.1 ms, processing 60.4 ms, presentation delay 293.5 ms.at her tapthe three parts, to scalenext frameThe quick version: the old order, at her tapThe quick version16 ms9.3 ms + 0.3 ms + 6.4 msThe quick version: the list sorted by price, at the next frameA busy page: the old order, at her tapA busy page258.7 msinput delay251.2 ms + 0.8 ms + 6.7 msA busy page: the list sorted by price, at the next frameAn extra listener: the old order, at her tapAn extra listener320 msprocessing11.4 ms + 301.6 ms + 7 msAn extra listener: the list sorted by price, at the next frameA long list: the old order, at her tapA long list368 mspresentation delay14.1 ms + 60.4 ms + 293.5 msA long list: the list sorted by price, at the next frame
Real frames and times from the recording. Asha saw the same two screens on every copy. The bars, to scale, show where the wait went: input delay, processing (shaded), then presentation delay. The busy page's leave out DevTools' hold.

The course compared the frames pixel by pixel. On every copy, her screen at the tap was the same picture. The next frame was the same sorted list. Why it matters: a visitor can't tell you which part was slow. Only the timings can.

Pick a copy below to see everything the recording kept for its tap. There's one more copy in the list, a long list measured in its handler. It comes up later.

Playground: one tap on each copy
The quick version8 rows

Chrome's entry: tap at 2,860.4 ms, handlers 2,869.7 ms to 2,870 ms, duration 16 ms.

  • input delay9.3 ms
  • processing0.3 ms
  • presentation delay6.4 ms

web-vitals: input delay 2.8 ms, processing 3.3 ms, presentation delay 1.9 ms. It found no long animation frame, so it names no script.

Long animation frames around the tap

None: nothing kept the main thread busy for 50 ms or more.

Try it yourself
16 ms, mostly input delay

The Sort handler only moves the rows. Nothing else is busy.

One tap on Sort by price in Chrome 157, with the CPU slowed 4 times. The entry is the click's; web-vitals 6.2.3 reads every event of the tap in the same frame. On the busy page, the parts leave out DevTools' hold.

Input delay: waiting for a busy main thread

Input delay is the wait before any of the tap's handlers can start. Some of it is always there. web.dev says it always takes a little time for the system to notice a tap and pass it to the browser. Even the quick version, with nothing else busy, waited 9.3 ms.

A long input delay has another cause. The main thread is busy with something else when the tap arrives. And the main thread does one task at a time. A task is one piece of work it picks up, like a timer's callback or a script. web.dev puts it plainly: each task runs until it finishes, however long it blocks the main thread.

Real-life analogyOne clerk at the post office

You go to the post office to buy one stamp. It takes the clerk five seconds. But the customer ahead of you is posting forty parcels. The clerk serves one person at a time. So you wait four minutes for a five-second job. Nothing about your stamp was slow.

In real life: The clerk
In the browser: The main thread
In real life: The customer with forty parcels
In the browser: analytics.js packing its notes: a long task
In real life: Buying your stamp
In the browser: The Sort handler: 0.8 ms of work
In real life: Waiting in the queue
In the browser: The input delay

That's the busy page. Here is its analytics.js:

analytics.js on the busy pageJavaScript
// analytics.js: every 5 seconds, pack up the notes and send them.(() => {  const busy = (ms) => { const end = performance.now() + ms; while (performance.now() < end) {} };  function flush() {    busy(300);    navigator.sendBeacon("/notes", "notes");    setTimeout(flush, 5000);  }  addEventListener("load", () => setTimeout(flush, 5000));})();
  • Line 1 says what the script does. Lines 2 and 10 wrap it, so its names stay private.
  • Line 3 is the stand-in for slow work: a loop that runs until the time is up.
  • Lines 4 to 8 are flush(). Line 5 packs the notes, for 300 ms. Line 6 sends them. Line 7 sets a timer to do it all again in 5 seconds.
  • Line 9 sets the first timer once the page has loaded.

Asha tapped 60 ms after flush() started. Her tap had to wait for the rest of it: 246.4 ms. Then the browser spent 4.8 ms on the tap's first events, like pointerdown. So the click's handlers started 251.2 ms after her tap. Then her handler ran for just 0.8 ms.

Chrome's own entry says 256.5 ms. The extra 5.3 ms is DevTools' hold. When flush() ended, DevTools waited for a frame before it passed the tap on. A real finger doesn't wait for that frame.

Why it matters: a faster Sort handler would change nothing here. It already took under a millisecond. The fix is in the other script.

Seeing who held the main thread

The entry says how long the wait was. It doesn't say who caused it. For that, use long animation frames, from lesson 11.5. A long animation frame is a stretch of 50 ms or more while the screen waited for its next picture. Chrome lists the scripts that ran in it.

The web-vitals library matches them to the tap for you. Its attribution build adds an attribution object to each INP report. Lesson 12.1 introduced it. Here is part of the report for the busy page:

web-vitals names the scriptPop out in the code editor (opens in a new tab)JavaScript
// web-vitals' attribution for the tap on the busy page. Times in ms.const attribution = {  inputDelay: 246.4, processingDuration: 5.6, presentationDelay: 6.7,  longestScript: {    subpart: "input-delay",    entry: { invoker: "TimerHandler:setTimeout", sourceURL: "analytics.js", sourceFunctionName: "flush" },  },};const { subpart, entry } = attribution.longestScript;console.log(`The longest script: ${entry.sourceFunctionName}() in ${entry.sourceURL}`);console.log(`Started by: ${entry.invoker}`);console.log(`It ran during the ${subpart}.`);
  • Line 3 holds web-vitals' three parts, without DevTools' hold. They add up to 258.7 ms, the whole tap.
  • Lines 4 to 7 hold longestScript: the longest script in the long animation frames around the tap. Line 5 says which part it fell in. Line 6 says what started it, its file, and its function.
  • Line 9 takes subpart and entry out of it. Lines 10 to 12 print The longest script: flush() in analytics.js, Started by: TimerHandler:setTimeout and It ran during the input-delay.

TimerHandler:setTimeout means a setTimeout timer started this script. web-vitals picks the part from when the script started. This one started before the handlers did, so it lands in input-delay. The recording agrees. flush() started 59.9 ms before the tap, and ran for 306.3 ms.

Look at line 3 again. web-vitals says 246.4 ms of input delay. The click's entry said 251.2. Both are right. web-vitals reads every event of the tap that reached the same frame, as web.dev recommends. It counts from the tap until the browser started on the first of them. Here, those were pointer events like pointerdown, just before the click. Why it matters: small gaps between tools are normal. Look at which part is biggest, not at the decimals.

In DevTools, the Interactions track draws input delay as a thin line on the left of each interaction. The docs call it a whisker. Lesson 7.2 showed it.

What usually makes it long

web.dev names these causes:

  • Timers that do a lot of work, like this analytics flush. web.dev warns that setInterval is more likely to get in the way than setTimeout, because it runs again and again.
  • Scripts running as the page starts. A tap during start-up waits behind them. Lesson 23.3 saw hydration do this: a framework making the server's HTML work.
  • Interactions that come close together. When you type fast, each key can wait for the last key's work.
  • Animations run in JavaScript that do a lot of work in every frame.

The fixes come in Module 27, The main thread. Break long tasks into short ones, and yield: pause, so the browser can handle a tap in between. Those lessons are in Responsiveness: INP.

Processing duration: every handler for the tap

Processing duration starts when the first handler starts. It ends when the last one ends. web.dev calls it the time the event callbacks take to run to completion.

"The handlers" can be more than you think. One tap fires several events, like pointerdown, pointerup and click. Each event can have many listeners. A listener is a function added with addEventListener. It's another word for a handler. It can sit on the button, or on the page around it. And any script can add one, yours or someone else's.

Real-life analogyA form that needs every desk's stamp

You hand in a form at the town hall. It goes from desk to desk, and each desk stamps it. Your desk is quick. But one desk also copies every form into a big book, by hand. You get your form back only after the last stamp. And you never chose that slow desk.

In real life: The form going round the desks
In the browser: The tap's events being handled
In real life: Each desk's stamp
In the browser: Each listener running
In real life: Your own quick desk
In the browser: Ravi's sortByPrice
In real life: The desk that copies every form by hand
In the browser: analytics.js's noteClick: 300 ms
In real life: Getting your form back
In the browser: The handlers end: processingEnd

That's the extra-listener copy. Its analytics.js adds one listener:

analytics.js with a click listenerJavaScript
// analytics.js: note every click on the page.(() => {  const busy = (ms) => { const end = performance.now() + ms; while (performance.now() < end) {} };  document.addEventListener("click", function noteClick() {    busy(300);  });})();
  • Line 4 adds a click listener to the document, the page itself. Clicks on a button travel up through the page to the document. So this listener hears every click.
  • Line 5 is the stand-in for its work: 300 ms.

So when Asha tapped Sort, two listeners ran. Ravi's ran first, on the button. Then the click reached the document, and noteClick ran. Chrome's entry put 301.6 ms between processingStart and processingEnd. Why it matters: processing isn't only your handler. Every listener for the tap's events adds to it.

Seeing whose code ran

Long animation frames name each script that ran for more than 5 ms. Here is the longest script from two taps. Both spent about 300 ms in processing:

Same part, two ownersPop out in the code editor (opens in a new tab)JavaScript
// The longest script in each tap's long animation frame. Durations in ms.const scripts = [  { copy: "The original", invoker: "BUTTON#sort.onclick", sourceURL: "shop.js", duration: 303.4 },  { copy: "An extra listener", invoker: "#document.onclick", sourceURL: "analytics.js", duration: 300.1 },];for (const script of scripts) {  console.log(`${script.copy}: ${script.invoker} in ${script.sourceURL}, ${script.duration} ms`);}
  • Line 3 is the original page from lesson 26.1, tapped on the slowed CPU. Its longest script was BUTTON#sort.onclick. That's the click listener on the button whose id is sort, from shop.js.
  • Line 4 is the extra-listener copy. Its longest script was #document.onclick: a click listener on the document, from analytics.js.
  • Lines 6 to 8 print The original: BUTTON#sort.onclick in shop.js, 303.4 ms and An extra listener: #document.onclick in analytics.js, 300.1 ms.

Same part, two owners. On the extra-listener copy, Ravi's sortByPrice isn't listed at all. It ran for less than 5 ms. web-vitals agreed: its longest script was #document.onclick, during the processing-duration. Why it matters: before you rewrite a handler, check whose code was slow.

In DevTools, the Interactions track draws processing as the solid bar between the two whiskers.

What usually makes it long

  • A handler that does too much before the screen can change: sorting big lists, saving, sending. web.dev advises doing only what the next frame needs, and the rest later.
  • Other scripts' listeners, like an analytics script's.
  • Building lots of HTML in JavaScript. web.dev warns that creating too much HTML at once on the client can hurt INP.
  • Reading a size right after changing the page. The next section shows why.

The fixes come in Module 28, Fast interactions: show the result first, and move the rest out of the way.

Presentation delay: drawing the new screen

Presentation delay starts when the last handler ends. It ends when the next frame is on the screen. web.dev and the web-vitals source list what can happen in between:

  • requestAnimationFrame callbacks, which run just before the browser draws;
  • ResizeObserver and IntersectionObserver callbacks;
  • style: working out which CSS rules apply to each element;
  • layout: working out each element's size and place;
  • paint, and the work after it to put the picture on the screen.

Lesson 6.3, The pixel pipeline, walked through style, layout and paint.

Real-life analogyRearranging the shop's shelves

Meera tells her staff to put the teas in price order. Saying it takes two seconds. Then they move every tin. With eight tins, it's done before you notice. With three thousand, the shop stays shut all afternoon. The instruction was short. The moving was the slow part.

In real life: Meera's instruction
In the browser: The handler: teas.append(...byPrice)
In real life: Moving every tin
In the browser: Style and layout for every row
In real life: Opening the doors again
In the browser: The next frame on the screen
In real life: Eight tins, or three thousand
In the browser: 8 rows, or 3,000

That's the long list. Its handler is the quick one from before. But it moves 3,000 rows, not 8. Chrome's entry gave the handler 60.4 ms. Then came 293.5 ms of presentation delay.

What the browser did after the handler

Here the long animation frame helps less. It named one script, BUTTON#sort.onclick, for 60.3 ms. Its rendering, from renderStart to the frame's end, took 1 ms. web-vitals left 288.1 ms of the tap unattributed: time it couldn't put on a script, on style and layout, or on paint.

So where did the time go? The trace answers that, because it records every step Chrome took. The course added up the main thread's work after the handler:

The long list, after the handlerPop out in the code editor (opens in a new tab)JavaScript
// The long list: the main thread's work after the handler, from Chrome's trace. Times in ms.const work = { "Recalculate style": 52.8, Layout: 176.4, "Pre-paint": 43.9, Paint: 10.2 };const presentationDelay = 293.5; const total = Object.values(work).reduce((sum, ms) => sum + ms, 0);console.log(`Rendering work: ${total.toFixed(1)} ms of ${presentationDelay} ms`);console.log(`Layout alone: ${work.Layout} ms`);
  • Line 2 holds four steps from the trace, with the names DevTools gives them. Recalculate style is the style step. Pre-paint gets ready for paint, as lesson 6.3 showed.
  • Line 3 is the presentation delay from Chrome's entry.
  • Line 5 adds up the steps. Lines 6 and 7 print Rendering work: 283.3 ms of 293.5 ms and Layout alone: 176.4 ms.
The long list's tap, 368 ms, to scale, three ways. Event Timing: input delay 14.1 ms, processing 60.4 ms, presentation delay 293.5 ms. The trace's Main track: the click handler for 60.4 ms, then Recalculate style 52.8 ms, Layout 176.4 ms, Pre-paint 43.9 ms, Paint 10.2 ms. The long animation frame: one script, BUTTON#sort.onclick, for 60.3 ms, then no script and no rendering until 348.4 ms, then 1 ms of rendering.Event Timing: the three partsprocessingpresentation delay 293.5 msThe trace's Main trackhandlerstylelayout 176.4 mspre-paintpaintThe long animation framescriptno script, no rendering namedrendering: 1 mstap100 ms200 ms300 msnext frame: 368 ms
The long list's tap, to scale. After the handler, the trace shows style, layout and pre-paint, in the same task as the handler. The long animation frame names only the script.

The trace shows one more thing. Chrome did the style, layout and pre-paint in the same task as the click, right after the handler. That was before renderStart, where the long animation frame starts counting rendering. Why it matters: when web-vitals leaves a big part unattributed, record a trace. Lesson 26.3 shows how to reproduce a slow tap in DevTools.

Lesson 11.4's rounding applies here, too. Chrome rounds each duration to the nearest 8 ms. Presentation delay is what's left of it, so it can be off by up to 4 ms.

When the handler asks for a size

The last copy is the long list again. This time, the handler also checks the list's height. It uses it to decide whether to show a Back to top link:

The handler reads a sizeJavaScript
const byPrice = [...teas.children].sort((a, b) => a.dataset.price - b.dataset.price);document.querySelector("#sort").addEventListener("click", function sortByPrice() {  teas.append(...byPrice);  document.querySelector("#top").hidden = teas.offsetHeight < innerHeight;});
  • Lines 1 to 3 are the quick handler again.
  • Line 4 reads teas.offsetHeight, the list's height in pixels. If the list is shorter than the window, it hides the link.
  • Line 5 closes the handler.

Line 4 has a cost. The rows have just moved, so Chrome doesn't know the list's height yet. To answer, it must work out style and layout right there, inside the handler. This is called a forced layout.

So the layout moved. The long animation frame says the script ran for 215.1 ms. Of that, 170.5 ms was forced style and layout. Chrome's entry gave processing 215.1 ms, and presentation delay only 47.1 ms.

Why it matters: the same layout can land in processing or in presentation delay. It depends on when it runs. Reading a size doesn't save the layout. It only moves it earlier. web.dev's INP guide lists this, layout thrashing, under making event callbacks faster.

What usually makes it long

  • A big page. web.dev says rendering work grows with the size of the DOM, the tree of elements on the page. Its guide quotes Lighthouse: over 1,400 nodes is too many.
  • Big changes, like adding or moving many elements at once.
  • Work in animation frame or resize callbacks, which run before the next frame.

web.dev says presentation delays can be the hardest to troubleshoot. Module 28 comes back to them, with a whole lesson on this part.

Now try all three causes in your own browser. Pick what to make slow, then press Sort. Watch which bar grows:

Playground: make one part slow
What this panel runs, shortenedJavaScript
// What the panel runs in each mode, shortened.if (mode === "busy page") setInterval(() => busy(150), 250);button.addEventListener("click", () => {  if (mode === "heavy handler") busy(150);  const list = mode === "long list" ? longList : teaList;  list.append(...[...list.children].reverse());});
Nothing slow0 timed

No taps timed yet.

    Try it yourself
    Which part grows?

    Pick what to make slow, then press Sort. Your browser times the tap and splits it into its three parts.

    Real times from your own browser, rounded to 8 ms. A busy page runs 150 ms of work every 250 ms for 15 seconds. A heavy handler works for 150 ms. A long list has 2,000 rows to draw again.

    Which phase to fix first

    Back to Ravi's question: what should he fix? web.dev's answer is short. How you speed up an interaction depends on which part takes the most time. So split the slowest interaction, and start with its biggest part.

    Real-life analogyCutting a long trip to work

    Asha's trip to work takes an hour. She walks five minutes to the bus. The bus takes fifty. Then she walks five more. Walking twice as fast saves her five minutes. A quicker bus route could save thirty. So she checks the timetable before she buys new shoes.

    In real life: The three legs of the trip
    In the browser: Input delay, processing, presentation delay
    In real life: The bus ride
    In the browser: The biggest part
    In real life: Walking faster
    In the browser: Fixing a small part: little changes
    In real life: Checking the timetable first
    In the browser: Splitting the tap before you fix it

    Here is the long list's tap, with each part taken away in turn:

    What if one part took no time?Pop out in the code editor (opens in a new tab)JavaScript
    // The long list's tap, in ms. What if one phase took no time at all?const tap = { "input delay": 14.1, processing: 60.4, "presentation delay": 293.5 };const total = Object.values(tap).reduce((sum, ms) => sum + ms, 0); for (const [phase, ms] of Object.entries(tap)) {  console.log(`Without ${phase}: ${(total - ms).toFixed(1)} ms`);}
    • Line 2 holds the three parts. Line 3 adds them up: 368 ms.
    • Lines 5 to 7 take each part away in turn. They print Without input delay: 353.9 ms, Without processing: 307.6 ms and Without presentation delay: 74.5 ms.

    Even an instant handler leaves the tap at 307.6 ms, still over 200. Only a fix to the drawing brings it under. Why it matters: a fix to the wrong part can be good work, and still not move INP.

    Try it on the other copies:

    Playground: take one part away
    A long list368 ms
    • Without input delay353.9 msneeds improvement
    • Without processing307.6 msneeds improvement
    • Without presentation delay74.5 msgood
    Try it yourself
    Fix the presentation delay first

    It's the biggest part. Without it, this tap would take 74.5 ms: good.

    Each copy's recorded tap, 368 ms in all, the busy page's without DevTools' hold. Each row asks: what if that one part took no time at all?

    Each part points to a different fix

    Read this table across. Each row is one part: where to look, what usually makes it long, and where the course fixes it.

    The three parts, and where each one leads
    Which part is long?What to look atUsual causesWhere the fix is taught
    Input delayThe long animation frame that was running at the tap. Its scripts started before the tap.Timers, scripts loading during start-up, other taps that came just before.Module 27, The main thread: break up long tasks and yield.
    ProcessingThe scripts in the frame that ran the handlers, and every listener for the tap's events.Heavy handlers, other scripts' listeners, reading sizes right after changing the page.Module 28, Fast interactions: show the result first, do the rest later.
    Presentation delayThe trace's Main track after the handlers: style, layout, paint.A big page to lay out again, large changes, animation frame and resize callbacks.Module 28, Fast interactions: change and draw less after each tap.

    What real sites see

    No part is always the biggest. The 2024 Web Almanac looked at the three parts on real sites. Its data came from RUMvision, a service that measures real visits. For the median interaction, the one in the middle, presentation delay was the biggest part, at 36 ms. For the slowest ones, input delay was. At the 90th percentile, it reached 155 ms. That's the value 90 in 100 interactions were at or under.

    Why it matters: you can't guess which part is slow on your own site. Measure it.

    Two more tips. First, start with your slowest interactions, because INP is the slowest one. Second, note when the tap happened. Input delays can be long while the page is still loading, when the main thread is often busiest. web-vitals reports when it happened as loadState. Lesson 26.3 shows how to find these taps in data from real visits.

    Which phase does it slow down?

    Each thing below makes a tap slower. Sort it: which part of the tap does it make longer?

    Which phase does it slow down?
    • A chat widget's timer runs for 200 ms just as the visitor taps
    • The visitor taps while a big script is still being run as the page loads
    • The visitor presses a second key while the first key's handler is still running
    • Another script's click listener does 200 ms of work
    • The handler filters 10,000 products in JavaScript
    • The handler reads offsetHeight right after changing the list
    • The handler is quick, but 3,000 rows must be laid out again
    • A requestAnimationFrame callback does 150 ms of work after the tap
    • A ResizeObserver callback does heavy work after the tap changes a size
    Try it yourself
    0 of 9 correct

    Sort each cause by the part of the tap it makes longer.

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

    Common mix-ups

    • "A slow interaction means a slow handler." No. On the busy page, the handler ran for 0.8 ms. The tap took 258.7 ms.
    • "Processing is only my own handler." No. It's every listener for the tap's events. On the extra-listener copy, analytics.js's listener took 300.1 ms.
    • "Presentation delay is the graphics card's job, so my code can't change it." No. The long list's rows made Chrome spend 176.4 ms on layout alone. Fewer rows, less layout.
    • "Reading a size in the handler pushes the slowness into presentation delay." No. It pulls the layout into the handler, so it counts as processing.
    • "Long animation frames always say where the time went." No. On the long list, web-vitals left 288.1 ms unattributed. Only the trace showed the layout.
    • "Making the slowest handler faster always fixes INP." No. On the long list, an instant handler still leaves 307.6 ms.
    • "The three parts are web-vitals' own idea." No. They come from the browser's own times. The Event Timing standard's example works them out the same way.

    Practice exercises

    Answer first, then use the hints.

    Exercise 1 · Warm-upSplit the measured list

    Here is Chrome's entry for the tap on the long list, measured. What does line 6 print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // Chrome's entry for the tap on the long list, measured. Times in ms.
    const tap = { startTime: 3270.6, processingStart: 3280.4, processingEnd: 3495.5, duration: 272 };
    
    console.log(`Input delay: ${(tap.processingStart - tap.startTime).toFixed(1)} ms`);
    console.log(`Processing: ${(tap.processingEnd - tap.processingStart).toFixed(1)} ms`);
    console.log(`Presentation delay: ${(tap.startTime + tap.duration - tap.processingEnd).toFixed(1)} ms`);

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

      Exercise 2 · Warm-upWithout the wait

      This asks the same question of the busy page's tap. What does the first line print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      // The busy page's tap, in ms. What if one phase took no time at all?
      const tap = { "input delay": 251.2, processing: 0.8, "presentation delay": 6.7 };
      const total = Object.values(tap).reduce((sum, ms) => sum + ms, 0);
      
      for (const [phase, ms] of Object.entries(tap)) {
        console.log(`Without ${phase}: ${(total - ms).toFixed(1)} ms`);
      }

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

        Exercise 3 · PracticeHow long the frame blocked

        In the first playground, pick An extra listener. What blocking duration does its long animation frame show?

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

          Exercise 4 · PracticeTake processing away

          In the take-one-part-away playground, pick An extra listener. What total does it show without processing?

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

            Exercise 5 · ChallengeHow many elements?

            Open the course's trace of the long list's tap in Chrome's DevTools. After the click handler, Chrome recalculated style. How many elements does DevTools say were affected?

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

              Exercise 6 · ChallengeOne frame, by the clock

              web-vitals groups a tap's events that reached the same frame. How close, in ms, must their render times be?

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

                Check your understanding

                For each question, ask when the work ran: before the handlers, during them, or after them.

                The three phases quiz · 7 questionsScore: first tries count
                1. Question 1 of 7This code reads one copy's entry and picks its biggest part. What does it print?

                  Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                  // Chrome's entry for one copy's tap. Times in ms.
                  const tap = { startTime: 2854.1, processingStart: 2865.5, processingEnd: 3167.1, duration: 320 };
                  const parts = {
                    "input delay": tap.processingStart - tap.startTime,
                    processing: tap.processingEnd - tap.processingStart,
                    "presentation delay": tap.startTime + tap.duration - tap.processingEnd,
                  };
                  const biggest = Object.keys(parts).reduce((a, b) => (parts[b] > parts[a] ? b : a));
                  console.log(`Fix the ${biggest} first.`);

                  Choose an answer to see the explanation.

                2. Question 2 of 7Asha taps Sort by price while a timer's callback from another script is still running. Which part of her tap grows?

                  Choose an answer to see the explanation.

                3. Question 3 of 7Ravi's click handler is quick, but another script listens for every click on the document and does slow work. Where does that work show up?

                  Choose an answer to see the explanation.

                4. Question 4 of 7A handler only moves the rows of a list, but the list has thousands of rows. Where does most of the tap's time go?

                  Choose an answer to see the explanation.

                5. Question 5 of 7A handler moves the rows, then reads the list's offsetHeight. Where does the layout time go now?

                  Choose an answer to see the explanation.

                6. Question 6 of 7This code asks what the long list's tap would take without each part. Which line would INP rate as good?

                  Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                  // The long list's tap, in ms. What if one phase took no time at all?
                  const tap = { "input delay": 14.1, processing: 60.4, "presentation delay": 293.5 };
                  const total = Object.values(tap).reduce((sum, ms) => sum + ms, 0);
                  
                  for (const [phase, ms] of Object.entries(tap)) {
                    console.log(`Without ${phase}: ${(total - ms).toFixed(1)} ms`);
                  }

                  Choose an answer to see the explanation.

                7. Question 7 of 7web-vitals' attribution left most of a slow tap's time unattributed. What's the best next step?

                  Choose an answer to see the explanation.

                Key takeaways

                • Every interaction is input delay, then processing, then presentation delay. Together they make its duration.
                • Input delay is waiting for a busy main thread. On the busy page, a 0.8 ms handler waited 251.2 ms.
                • Processing is every listener for the tap's events, yours or not. Long animation frames name the script.
                • Presentation delay is the drawing after the handlers. Big pages and big changes make it long. A trace shows the steps.
                • Split the slowest interaction, and fix its biggest part first.

                Remember the one-liner.
                Split the slow tap into its three waits, and fix the biggest one first.

                Sources

                The facts on this page come from these specs, docs and source files:

                Coming next: Finding slow interactions. It finds the slow taps in data from real visits, then reproduces them in DevTools.

                CompleteFrontend Clear concepts. Working examples.