completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

Event Timing and INP

How the Event Timing API reports slow interactions, and how INP is computed from it. A test page clicked and typed in by real input in Chrome, Firefox and WebKit, with three observers and every entry they got.

By the end, you can
  • 01
    Read an event entrySay what startTime, processingStart, processingEnd and duration mean, and choose a durationThreshold so the slow events reach you.
  • 02
    Find where a click's time wentGroup entries by interactionId, take the longest as the interaction's latency, and split it into input delay, processing and presentation delay.
  • 03
    Work out INP yourselfPick the slowest interaction, skip one for every 50, and count them all with performance.interactionCount.

Every slow event, on the timeline

Asha is at her laptop, on the Leaf and Kettle page. She clicks Like under the mint tea, and the button changes to Liked straight away. She clicks Add to cart. The cart count goes up a moment later. Then she types "mint" in the search box.

A little later, a script on the page gets busy for a quarter of a second. Right then, Asha clicks Like again, to undo it. This time nothing happens at first. Then the button changes back.

Same button, same code. So why was the second click slow? And if it happened on Asha's laptop, not on yours, how would you ever find out?

The browser knows. It times every click, tap and key press, from the moment it happens until the screen shows the result. A standard called Event Timing hands those times to your page. The responsiveness metric Interaction to Next Paint (INP) is built from them. You met INP in Core Web Vitals: 200 ms or less is good. This lesson follows Asha's clicks from the first timing all the way to INP.

Here is how Chrome timed Asha's first click on Like, with her screen at four moments of it:

Asha's screen at four moments of her first click on Like, in Chrome. At 0 ms, Asha clicks the button: the button says "Like". At 9.5 ms, the handler starts: the button says "Like". At 10 ms, the handler ends: the button says "Like". At 40 ms, the next frame is drawn: the button says "Liked". Between the screens are three waits: input delay 9.5 ms, processing 0.5 ms, presentation delay 30 ms. 40 ms in all.0 msThe page: the button says "Like", and Add to cart is under itAsha clicksthe button9.5 msThe page: the button says "Like", and Add to cart is under itthe handlerstarts10 msThe page: the button says "Like", and Add to cart is under itthe handlerends40 msThe page: the button says "Liked", and Add to cart is under itthe next frameis drawninput delay9.5 msprocessing0.5 mspresentation delay30 ms40 ms from the click to the next frame
Asha's screen at four moments of her first click on Like. The screens are real frames Chrome drew when the course repeated the click with screenshots on. The button only changed at the next frame, 40 ms after the click. The three cards are the waits in between. The screens are evenly spaced, not to scale. Shaded: the click handler running.

Her screen changed only at the last moment, 40 ms after her click, when the browser drew its next frame. Before that, the click went through three waits, one after the other. This lesson is about those waits.

The course built a small copy of the page to time. It has a Like button, an Add to cart button and a search box. Three things on it are slow on purpose:

  • Add to cart's click handler keeps the main thread busy for 120 ms.
  • The search box's keydown handler does 40 ms of work for every key.
  • Just before the second Like click, a 250 ms task starts. That's the busy script.

The main thread is the one thread that runs the page's JavaScript and draws its frames. A frame is one new picture on the screen. Lesson 6.2, The event loop and frames, compared the main thread to a cook who makes one order at a time.

Then the course used Playwright, a tool that drives a real browser, to do what Asha did. Playwright sends its clicks and key presses through the browser's own input, so the browser treats them as a person's. It ran the same steps in Chrome, and in Playwright's builds of Firefox and WebKit, Safari's engine.

Each click and each key press is one interaction. Typing "mint" is four key presses, so the visit had eight interactions in every browser. Here is what Chrome wrote down for the first click on Like. One click fires several events, and this is the entry for the one called click:

Three waits in one clickPop out in the code editor (opens in a new tab)JavaScript
// Chrome's entry for the click event of the first click on Like. Times in ms.const click = { startTime: 593.5, processingStart: 603, processingEnd: 603.5, duration: 40 };const nextFrame = click.startTime + click.duration; console.log(`It waited ${click.processingStart - click.startTime} ms for its handler to start.`);console.log(`Its handler ran for ${click.processingEnd - click.processingStart} ms.`);console.log(`Then it waited ${nextFrame - click.processingEnd} ms for the next frame.`);
  • Line 1 is a comment. Times are in milliseconds (ms), counted from when the page started loading.
  • Line 2 holds four numbers from the entry. startTime is when the click happened. processingStart is when its handler was about to run, and processingEnd is when it finished. duration is how long it took from the click until the next frame was drawn.
  • Line 3 adds the duration to the start time. That gives the moment the next frame was ready.
  • Lines 5 to 7 subtract each time from the next. They print It waited 9.5 ms for its handler to start., Its handler ran for 0.5 ms. and Then it waited 30 ms for the next frame.

Add the three up: 9.5 + 0.5 + 30 = 40 ms, the whole duration. They're the three waits between Asha's four screens in the first picture.

A café counter works the same way:

Real-life analogyRinging the bell at a café counter

You walk up to the counter at Leaf and Kettle and ring the little bell. First you wait for the cook to hear it, because she may be finishing someone else's order. Then she makes your tea. Then the cup waits for the waiter's next round, and he carries it to your table. How long did your order take? All three waits, added up.

In real life: Ringing the bell
In the browser: The click, at the entry's startTime
In real life: Waiting for the cook to hear it
In the browser: Input delay: from startTime to processingStart
In real life: The cook making your tea
In the browser: Processing: the handlers run, from processingStart to processingEnd
In real life: Waiting for the waiter's round to bring it
In the browser: Presentation delay: from processingEnd until the next frame
In real life: From the bell to the cup on your table
In the browser: The entry's duration

It's the kitchen from lesson 6.2. The cook is the main thread, and the waiter's rounds are the frames. The three waits have names. Together, they make the interaction's latency: the time from the input to the next frame.

  • Input delay: from the click until its handlers start.
  • Processing: while the handlers run. web.dev calls it processing duration.
  • Presentation delay: from the end of the handlers until the next frame is drawn.

That next frame is the "next paint" in Interaction to Next Paint. The first Like click took 40 ms in all. The second one, during the busy script, took 216 ms in Chrome. Yet its handler ran for just 0.1 ms. So the time went on one of the other two waits, and its entry says which.

The main idea

The browser times every click, tap and key press, from the input to the next frame. The events from one input form one interaction. INP is the slowest interaction of the visit, ignoring one in every 50. Each interaction splits into three waits, so you can see whether the time went on waiting, on your code, or on drawing.

Getting from one click to INP takes four steps. The next four sections take one each:

  1. Read the event entries, and choose which ones the browser hands you.
  2. Find the events that belong to one click, with interactionId.
  3. Group them into interactions, and split each one into its three waits.
  4. Pick INP from all the interactions.

Event entries and durationThreshold

Each time the browser runs an event for a click, a tap or a key, it can write an event entry about it. That's one line in the page's performance timeline, the logbook from lesson 11.1, The performance timeline. In JavaScript, it's a PerformanceEventTiming object. It has the usual parts of an entry, and some of its own:

  • entryType is "event".
  • name is the event's type, like click or keydown.
  • startTime is when the input happened.
  • processingStart and processingEnd are when the event's handlers started and finished.
  • duration runs from startTime until the browser next finished drawing, after the handlers ran.
  • interactionId says which click or key press the event belongs to. The next section is about it.
  • target is the element the event happened on, like the Like button.

Only certain events a person causes get an entry, such as clicks, key presses and typing. The standard lists them all. An event your own script fires, like button.click(), never gets one. The standard skips any event whose isTrusted is false, which means a script made it. That's why the course used Playwright: its clicks count as a person's.

Only the slow ones reach you

A page gets a lot of events, and most of them are quick. So the browser doesn't hand you every entry. You ask for them with an observer, and say how slow an event must be before you hear about it. Lesson 11.1 compared an observer to a tap on the shoulder. This time, the tap comes with a limit:

Real-life analogyOnly tell me about the slow orders

Ravi times every order at Leaf and Kettle, from the bell to the cup on the table. You ask him to tap you on the shoulder about the slow ones: "anything over five minutes". The quick orders still happen. You just never hear about them. Give him no number, and Ravi uses his own limit, a high one. Then you only hear about the really slow orders.

In real life: Ravi, timing every order
In the browser: The browser, timing every event
In real life: "Tap me for anything over five minutes"
In the browser: durationThreshold: the shortest duration you want to hear about
In real life: His own limit, when you give none
In the browser: The default: 104 ms
In real life: The quick orders you never hear about
In the browser: Events under the limit: they happen, but your observer gets no entry

In code, the limit is called durationThreshold:

Ask for every event of 16 ms or moreJavaScript
// Tell me about every event that took 16 ms or more, from the input to the next frame.const observer = new PerformanceObserver((list) => {  for (const entry of list.getEntries()) {    console.log(`${entry.name} took ${entry.duration} ms`);  }});observer.observe({ type: "event", buffered: true, durationThreshold: 16 });
  • Lines 2 to 6 make an observer, as in lesson 11.1. Line 4 prints each entry's name and duration.
  • Line 7 starts it. type: "event" asks for event entries. durationThreshold: 16 asks for every event of 16 ms or more.
  • buffered: true, also on line 7, brings back entries written before the observer started.

The code only prints when someone clicks or types, so it's shown here, not run. Three rules decide what it hears:

  • Without durationThreshold, an observer only hears about events of 104 ms or more.
  • With it, the limit is your number, but never below 16 ms. Ask for 5, and you get 16.
  • buffered: true only brings back earlier entries of 104 ms or more. The browser doesn't keep the shorter ones. So start your observer early, before anyone clicks.

The course's page had both kinds of observer, side by side. Here's what each one got in Chrome, for the same five steps:

Two observers, two limitsPop out in the code editor (opens in a new tab)JavaScript
// How many entries each of Chrome's two observers got, for the same five steps.const got = { lowThreshold: 54, defaultThreshold: 34 }; console.log(`The observer set to 16 ms got ${got.lowThreshold} entries.`);console.log(`The observer with the default got ${got.defaultThreshold}.`);console.log(`The default missed ${got.lowThreshold - got.defaultThreshold}.`);
  • Line 2 holds the two recorded counts.
  • Lines 4 to 6 print The observer set to 16 ms got 54 entries., The observer with the default got 34. and The default missed 20.

Which 20? The 12 entries of the first Like click, 40 ms each, and the 8 entries of the four key presses, 40 to 56 ms each. Each key press kept the main thread busy for at least 40 ms, and the default observer never mentioned one. Firefox's and WebKit's default observers missed every key press too.

Why it matters: if every click on a page took under 104 ms, a default observer would report nothing at all. You couldn't work out INP from it. To see those interactions, set durationThreshold yourself.

Durations come in steps of 8 ms

Look at any recorded duration: 40, 56, 120, 128, 216. Every one is a multiple of 8. The standard rounds each duration "to the nearest 8ms". It's like the kitchen clock from lesson 11.1, which moves in steps, not smoothly.

Why round at all? Lesson 11.1 showed that browsers keep their clocks a little coarse for safety, so pages can't time things too finely. An exact duration would be a new, finer clock. Steps of 8 ms keep it coarse. They're still fine enough for a 120 Hz screen, where a frame lasts about 8.3 ms. Here is the rounding in code:

Rounding to the nearest 8 msPop out in the code editor (opens in a new tab)JavaScript
// Event Timing rounds every duration to the nearest 8 ms.const roundTo8 = (ms) => Math.round(ms / 8) * 8; console.log(`A wait of 101 ms is written as ${roundTo8(101)} ms.`);console.log(`A wait of 99 ms is written as ${roundTo8(99)} ms.`);
  • Line 2 divides by 8, rounds to a whole number, and multiplies by 8 again.
  • Line 4 prints A wait of 101 ms is written as 104 ms. Line 5 prints A wait of 99 ms is written as 96 ms.

That explains the odd default. The standard calls 104 "just the first multiple of 8 greater than 100ms". A duration written as 104 or more really took at least 100 ms. The standard says an event that slow "will likely negatively impact user experience."

The first input is always kept

One entry ignores the limit. The page's first input is its first event with an interactionId: usually the first click, tap or key press. The browser copies that entry as a second type, first-input, and hands it over however short it was. It's the entry behind First Input Delay, the metric INP replaced.

The course's page had a third observer, for first-input. In all three engines, it got the pointerdown of the first Like click. In Firefox and WebKit, that took just 8 ms. That's too short for the 16 ms observer, so only the first-input observer heard about it. In Chrome, it took 40 ms.

interactionId: one number for one click

Back to Asha's first click on Add to cart. To her, it was one thing. Chrome wrote eleven entries for it. Before reading them, picture an old shop bell:

Real-life analogyOne press, three rings

The bell on the café counter is an old one. It rings once when you press it down, again when you let go, and a third time, on a different note, to say "order placed". The cook hears three rings, but she knows it's one customer. She writes the same ticket number next to all three. Someone who just walks past the counter gets no number at all.

In real life: Pressing the bell down
In the browser: pointerdown
In real life: Letting it go
In the browser: pointerup
In real life: The "order placed" ring
In the browser: click
In real life: The same ticket number next to all three
In the browser: interactionId: one number shared by the events of one click
In real life: Walking past the counter
In the browser: Moving the mouse over things: interactionId 0

Every event entry has an interactionId. The events that came from the same click or key press share one. Every other event gets 0. Here are Chrome's eleven entries, with their IDs:

Eleven entries, one clickPop out in the code editor (opens in a new tab)JavaScript
// Chrome's entries for the first click on Add to cart: [event, interactionId].const entries = [  ["pointerout", 0], ["pointerleave", 0], ["pointerover", 0], ["pointerenter", 0], ["mouseout", 0], ["mouseover", 0],  ["pointerdown", 1055], ["mousedown", 0], ["pointerup", 1055], ["mouseup", 0], ["click", 1055],];const inTheClick = entries.filter(([name, id]) => id !== 0); console.log(`In the interaction: ${inTheClick.map(([name]) => name).join(", ")}`);console.log(`Left out: ${entries.length - inTheClick.length} events with interactionId 0`);
  • Lines 2 to 5 list the entries in the order Chrome wrote them: each event's name, then its interactionId.
  • Line 6 keeps the entries whose ID isn't 0.
  • Line 8 prints In the interaction: pointerdown, pointerup, click. Line 9 prints Left out: 8 events with interactionId 0.
Chrome's entries for the first Add to cart click: pointerout (interactionId 0), pointerleave (interactionId 0), pointerover (interactionId 0), pointerenter (interactionId 0), mouseout (interactionId 0), mouseover (interactionId 0), pointerdown (interactionId 1055), mousedown (interactionId 0), pointerup (interactionId 1055), mouseup (interactionId 0), click (interactionId 1055). The events with interactionId 1055 are one interaction, with a latency of 128 ms.pointeroutpointerleavepointeroverpointerentermouseoutmouseoverpointerdownmousedownpointerupmouseupclickinteraction 1055: latency 128 ms
Chrome's 11 entries for one click on Add to cart. The 3 shaded ones share interactionId 1055: one interaction. The 8 crossed out have interactionId 0.

Six of the eight left out are the mouse moving from Like to Add to cart: out of one button, over the other. The page has no handlers for them, yet they were timed. The standard times an event whether or not anything listens to it. Moving the mouse isn't an interaction, though, and neither is scrolling or zooming. INP ignores them.

The other two are mousedown and mouseup. They belong to the click, but they got 0 as well. The standard gives an ID to only a few event types:

  • For a click or a tap: pointerdown, pointerup and click.
  • For a key press: keydown and keyup. In the recording, every engine also gave keypress the same ID as its keydown. web.dev counts all three as one keystroke.

And the numbers themselves? Chrome's IDs went 1048, 1055, 1062 and on. Firefox's started at 8765, and WebKit's at 8558. All three went up by 7 each time. The standard starts each page at a random number between 100 and 10,000, and steps up by a small amount. That's on purpose: it stops people using the IDs to count interactions. There's a proper counter for that, and the INP section uses it.

Grouping events into interactions

Now put the entries with the same interactionId together. Each group is one interaction. But how long did the interaction take? Each event in the group has its own duration, and they don't always agree. web.dev's rule: "The event with the longest duration within the interaction is what contributes to the interaction's total latency."

An interaction lasts as long as its longest eventPop out in the code editor (opens in a new tab)JavaScript
// The three events of that click, with their durations in ms.const click = { pointerdown: 128, pointerup: 120, click: 120 }; const latency = Math.max(...Object.values(click));console.log(`The interaction's latency is ${latency} ms.`);
  • Line 2 holds the durations of the click's three events, in ms.
  • Line 4 takes the largest. Line 5 prints The interaction's latency is 128 ms.

In your own code, keep one number per interactionId: the longest duration seen so far. Update it as each entry arrives. The Event Timing standard's own example does exactly that.

Where did the time go?

A latency tells you that an interaction was slow. It doesn't tell you why. For that, split it into the three waits from the café counter. Read this table across: each row is one wait, the two times around it, and what usually makes it long.

The three parts of an interaction
The waitIt starts atIt ends atWhat usually makes it long
Input delaystartTime: the clickprocessingStart: the handlers startOther work on the main thread, like a long task, that the click waits behind.
ProcessingprocessingStartprocessingEnd: the handlers finishThe page's own event handlers doing a lot of work.
Presentation delayprocessingEndThe next frame: startTime + durationThe browser working out styles and layout, and painting the new frame.

Each part is one subtraction between two of the entry's times. The course's page had two slow clicks, slow for opposite reasons. First, Add to cart:

Add to cart: a slow handlerPop out in the code editor (opens in a new tab)JavaScript
// Chrome's entry for the click event on Add to cart. Times in ms.const click = { startTime: 1052.4, processingStart: 1054.4, processingEnd: 1174.5 }; console.log(`It waited ${(click.processingStart - click.startTime).toFixed(1)} ms for its handler.`);console.log(`Its handler ran for ${(click.processingEnd - click.processingStart).toFixed(1)} ms.`);
  • Line 2 holds three times from Chrome's entry for the click.
  • Line 4 prints It waited 2.0 ms for its handler. Line 5 prints Its handler ran for 120.1 ms.

The handler started almost at once. Then it worked for 120 ms, as the page told it to. Nearly all of the time was processing, like a cook taking a long time over one order. Why it matters: here, the fix is inside the handler. Make it do less. The Responsiveness: INP stage shows how.

Now Asha's second click on Like, during the 250 ms task:

The busy Like click: a long waitPop out in the code editor (opens in a new tab)JavaScript
// Chrome's entry for the click event on Like, during a 250 ms task. Times in ms.const click = { startTime: 2633.1, processingStart: 2851.6, processingEnd: 2851.7 }; console.log(`It waited ${(click.processingStart - click.startTime).toFixed(1)} ms for its handler.`);console.log(`Its handler ran for ${(click.processingEnd - click.processingStart).toFixed(1)} ms.`);
  • Line 2 holds the same three times, from the busy Like click.
  • Line 4 prints It waited 218.5 ms for its handler. Line 5 prints Its handler ran for 0.1 ms.

That answers the question from the start. The Like handler was as quick as ever. The click spent 218.5 ms in input delay, because the main thread was still running the long task. Lesson 6.2 showed why: a task runs to its end before the next one starts. It's the cook who can't hear the bell until she finishes a big order.

Making the Like handler faster would change nothing here. The fix is to break the long task into shorter ones, so the click gets its turn sooner. The picture puts the two clicks side by side, to scale:

Two slow clicks in Chrome: Add to cart, input delay 2 ms, processing 120.1 ms, duration 120 ms; Like, during a long task, input delay 218.5 ms, processing 0.1 ms, duration 216 ms.Add to cartprocessing120 mswaited 2 ms, ran 120.1 msLike, during a long taskinput delay216 mswaited 218.5 ms, ran 0.1 ms
Two slow clicks in Chrome, drawn to scale. Add to cart spent its time running its handler. The busy Like click spent it waiting for the handler to start.

These examples read one entry per click, the click entry. web.dev suggests reading all the entries handled in the same frame together, from the first handler's start to the last one's end. The web-vitals library works it out that way. For these two clicks, it gives the same answer: one ran long, and one waited long.

When a wait comes out below zero

Now try the third wait on the busy click. Its duration was 216 ms. Yet its handler only started 218.5 ms after the click. Something doesn't add up:

A presentation delay below zeroPop out in the code editor (opens in a new tab)JavaScript
// The same click, with its duration, rounded to the nearest 8 ms. Times in ms.const click = { startTime: 2633.1, processingEnd: 2851.7, duration: 216 }; const presentationDelay = click.startTime + click.duration - click.processingEnd;console.log(`Presentation delay: ${presentationDelay.toFixed(1)} ms`);console.log(`Kept at zero or more: ${Math.max(0, presentationDelay).toFixed(1)} ms`);
  • Line 2 holds the click's start, the end of its handlers, and its duration.
  • Line 4 works out the presentation delay: the end of the duration, minus the end of the handlers.
  • Line 5 prints Presentation delay: -2.6 ms. Line 6 keeps it at zero or more, and prints Kept at zero or more: 0.0 ms.

A wait can't really be negative. The cause is the rounding. The frame came after the handler, so the real duration was at least 218.6 ms. Rounded to the nearest 8, it became 216. The standard's own example expects this. It labels presentation delay "approximate" and wraps it in Math.max(0, …), as line 6 does. Any presentation delay you work out can be off by up to 4 ms.

On the course's page, each click changed one word or one number, so there was little to draw. When a click changes much more of the page, the browser has more to lay out and paint before the next frame, and this wait grows.

Computing INP yourself

Asha's visit had eight interactions, each with its latency. INP is one number for the whole visit. Which one does it pick?

Real-life analogyA café rated by its slowest order

A food guide sends an inspector to Leaf and Kettle for a whole day. At closing time, she doesn't write down the average wait, because twenty quick orders can hide one customer who waited ten minutes. She writes down the slowest. On a very busy day, she lets the café off one slow order for every 50, in case it was a fluke.

In real life: Each order, from the bell to the table
In the browser: Each interaction, with its latency
In real life: The one number in the guide
In the browser: INP
In real life: The slowest order of the day
In the browser: The slowest interaction of the visit
In real life: One order let off for every 50
In the browser: One interaction ignored for every 50
In real life: The day's full count of orders, quick ones too
In the browser: performance.interactionCount

web.dev puts the rule like this: "The final INP value is the longest interaction observed, ignoring outliers." And for the outliers: "we ignore one highest interaction for every 50 interactions." So:

  • With fewer than 50 interactions, INP is the slowest one.
  • With 50 to 99, it's the second slowest.
  • With 100 to 149, it's the third slowest, and so on.

Most visits don't have over 50 interactions, web.dev says, so INP is usually the slowest. Why not the average? INP is meant to be a value that all, or nearly all, of the visit's interactions were under. A low INP means the page answered quickly almost every time. Here is Chrome's visit:

INP from Chrome's eight interactionsPop out in the code editor (opens in a new tab)JavaScript
// The latency of each of Chrome's interactions, in order, in ms.const latencies = [40, 128, 56, 40, 40, 40, 216, 120];const interactionCount = 8; const slowestFirst = [...latencies].sort((a, b) => b - a);const skip = Math.floor(interactionCount / 50);console.log(`INP: ${slowestFirst[skip]} ms`);
  • Line 2 holds the latency of each of Chrome's interactions, in the order they happened.
  • Line 3 holds how many interactions the visit had.
  • Line 5 sorts a copy of the list, slowest first.
  • Line 6 works out how many to skip: one for every 50, rounded down. For 8 interactions, that's 0.
  • Line 7 prints INP: 216 ms.

That's the busy Like click. The average of the eight is 85 ms, which looks fine. INP shows the one click that wasn't. 216 ms is over the 200 ms limit for good. So one click during a long task was enough to make this visit "needs improvement".

Count every interaction, not just the slow ones

Line 3 needs care. Your observer only hears about slow events, so it misses the quick interactions. If you count only what it reported, you skip too few:

Which count decides the skip?Pop out in the code editor (opens in a new tab)JavaScript
// An observer reported 40 slow interactions. The visit had 120 in all.const reported = 40;const interactionCount = 120; console.log(`Counting only the reported ones, skip ${Math.floor(reported / 50)}.`);console.log(`Counting every interaction, skip ${Math.floor(interactionCount / 50)}.`);
  • Lines 2 and 3 hold two counts for one made-up visit: 40 interactions reported, and 120 in all.
  • Line 5 prints Counting only the reported ones, skip 0. Line 6 prints Counting every interaction, skip 2.

Skipping too few makes INP look worse than it was. The browser keeps the full count for you. performance.interactionCount "represents the number of real-user interactions that have occurred on the page since it was loaded", quick ones included. On the course's page, it said 8 in every engine.

INP is only final when the visit ends. web.dev says it's "calculated when the user leaves the page". Until then, a later click could still be the slowest.

Here are the interactions each engine reported to its 16 ms observer. Each bar is one interaction, split into its three waits, and the bold row is INP. Pick each engine in turn and look for the busy Like click:

Playground: every interaction, by phase
Interactions0 to 240 ms
Click Like40 ms
Click Add to cart128 ms
Type "mint" in Search56 ms
Type "mint" in Search40 ms
Type "mint" in Search40 ms
Type "mint" in Search40 ms
Click Like during a long task216 ms
Click Add to cart120 ms

input delay processing presentation delay

Try it yourself
Chrome 156: INP 216 ms

The observer set to 16 ms reported 8 of the 8 interactions that performance.interactionCount counted. Each bar is one interaction's click or keydown entry: input delay, then processing, then presentation delay. The bold row is INP. With fewer than 50 interactions, that's the slowest one.

The same five steps in each engine, with Playwright's real mouse and keyboard, in a fresh browser on an Apple M1 Pro.

The engines ran the same steps, but they didn't report the same interactions, or the same INP. The exercises ask you to read them.

Which phase is slow?

Each cause below makes an interaction slow. Sort it by the wait it makes longer. Ask one question: did it happen before the handlers started, while they ran, or after they finished?

Which phase is slow?
  • A long task was running when the person clicked
  • A timer's callback took the main thread just before the tap
  • The click handler itself does 120 ms of work
  • Each keydown handler runs for 40 ms
  • The handler is quick, but the new frame takes long to draw
  • The click inserts thousands of DOM nodes, laid out before the next paint
Try it yourself
0 of 6 correct

Sort each cause by the wait 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. Chrome's busy Like click took 216 ms, and its handler ran for 0.1 ms. The rest was input delay.
  • "Each event is an interaction." No. One click on Add to cart gave eleven entries in Chrome. Three of them shared an ID and made one interaction.
  • "An observer sees every slow event." No. Without durationThreshold, it only hears about events of 104 ms or more. It missed every key press in the search box.
  • "Durations are exact." No. They're rounded to the nearest 8 ms. That's why a presentation delay can come out below zero.
  • "INP is the average interaction." No. It's the slowest, ignoring one for every 50. Chrome's average was 85 ms, and its INP was 216 ms.
  • "interactionId counts the interactions." No. It starts at a random number and steps up by a small amount. Count them with performance.interactionCount.

Practice exercises

Answer first, then use the hints.

Exercise 1 · Warm-upMore interactions

The same eight latencies came from a visit with 60 interactions. What does the code print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
// The latency of each of Chrome's interactions, in order, in ms.
const latencies = [40, 128, 56, 40, 40, 40, 216, 120];
const interactionCount = 60;

const slowestFirst = [...latencies].sort((a, b) => b - a);
const skip = Math.floor(interactionCount / 50);
console.log(`INP: ${slowestFirst[skip]} ms`);

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

    Exercise 2 · Warm-upA faster handler

    Add to cart's handler is cut down to 30 ms. What does the second line print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // Chrome's entry for the click event on Add to cart. Times in ms.
    const click = { startTime: 1052.4, processingStart: 1054.4, processingEnd: 1084.4 };
    
    console.log(`It waited ${(click.processingStart - click.startTime).toFixed(1)} ms for its handler.`);
    console.log(`Its handler ran for ${(click.processingEnd - click.processingStart).toFixed(1)} ms.`);

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

      Exercise 3 · PracticeFirefox's interactions

      How many interactions did Firefox's 16 ms observer report entries for?

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

        Exercise 4 · PracticeWebKit's slowest

        How long was WebKit's slowest interaction?

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

          Exercise 5 · ChallengeNo handlers at all

          The page has no handlers for moving the mouse, yet those events were timed. In the Event Timing standard, what does processingEnd equal when an event has no handlers?

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

            Exercise 6 · ChallengeLeft out on purpose

            The Event Timing standard leaves out some events because they're "continuous". Name one that a mouse fires.

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

              Check your understanding

              For each question, ask which events belong together, which wait took the time, and how many interactions there were.

              Event Timing and INP quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does the INP example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // The latency of each of Chrome's interactions, in order, in ms.
                const latencies = [40, 128, 56, 40, 40, 40, 216, 120];
                const interactionCount = 8;
                
                const slowestFirst = [...latencies].sort((a, b) => b - a);
                const skip = Math.floor(interactionCount / 50);
                console.log(`INP: ${slowestFirst[skip]} ms`);

                Choose an answer to see the explanation.

              2. Question 2 of 7What does the first line of the busy Like click example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // Chrome's entry for the click event on Like, during a 250 ms task. Times in ms.
                const click = { startTime: 2633.1, processingStart: 2851.6, processingEnd: 2851.7 };
                
                console.log(`It waited ${(click.processingStart - click.startTime).toFixed(1)} ms for its handler.`);
                console.log(`Its handler ran for ${(click.processingEnd - click.processingStart).toFixed(1)} ms.`);

                Choose an answer to see the explanation.

              3. Question 3 of 7Why did a quick Like handler give a 216 ms click in Chrome?

                Choose an answer to see the explanation.

              4. Question 4 of 7A click fires pointerdown, mousedown, pointerup, mouseup and click. How many interactions is that?

                Choose an answer to see the explanation.

              5. Question 5 of 7An observer with the default durationThreshold never reports your 40 ms keydowns. Why?

                Choose an answer to see the explanation.

              6. Question 6 of 7Chrome's busy Like click has a presentation delay of -2.6 ms. Why below zero?

                Choose an answer to see the explanation.

              7. Question 7 of 7Firefox reported entries for 6 interactions, but performance.interactionCount was 8. Which is right?

                Choose an answer to see the explanation.

              Key takeaways

              • The browser writes an event entry for an input's events, timed from the input to the next frame and rounded to the nearest 8 ms.
              • An observer hears about events of 104 ms or more by default. Set durationThreshold to go as low as 16.
              • Events from one click or key press share an interactionId. The interaction's latency is the longest of their durations.
              • Split a slow interaction into input delay, processing and presentation delay before you fix anything. Each has its own cause.
              • INP is the slowest interaction, ignoring one for every 50. Count them all with performance.interactionCount.

              Remember the one-liner.
              Group by interactionId, split into three waits, and take the slowest.

              Sources

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

              Coming next: Long Tasks and Long Animation Frames, also in Measuring real users. Event Timing tells you a click waited. The next lesson finds what it waited for, down to the script that caused it.

              CompleteFrontend Clear concepts. Working examples.