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.
- 01Read an event entrySay what startTime, processingStart, processingEnd and duration mean, and choose a durationThreshold so the slow events reach you.
- 02Find 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.
- 03Work 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:
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:
// 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.
startTimeis when the click happened.processingStartis when its handler was about to run, andprocessingEndis when it finished.durationis 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.andThen 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:
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
startTimetoprocessingStart - In real life: The cook making your tea
- In the browser: Processing: the handlers run, from
processingStarttoprocessingEnd - In real life: Waiting for the waiter's round to bring it
- In the browser: Presentation delay: from
processingEnduntil 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 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:
- Read the event entries, and choose which ones the browser hands you.
- Find the events that belong to one click, with
interactionId. - Group them into interactions, and split each one into its three waits.
- 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:
entryTypeis"event".nameis the event's type, likeclickorkeydown.startTimeis when the input happened.processingStartandprocessingEndare when the event's handlers started and finished.durationruns fromstartTimeuntil the browser next finished drawing, after the handlers ran.interactionIdsays which click or key press the event belongs to. The next section is about it.targetis 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:
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:
// 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: 16asks 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: trueonly 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:
// 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.andThe 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:
// 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 printsA 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:
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:
interactionId0
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:
// 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 printsLeft out: 8 events with 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,pointerupandclick. - For a key press:
keydownandkeyup. In the recording, every engine also gavekeypressthe same ID as itskeydown. 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."
// 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 wait | It starts at | It ends at | What usually makes it long |
|---|---|---|---|
| Input delay | startTime: the click | processingStart: the handlers start | Other work on the main thread, like a long task, that the click waits behind. |
| Processing | processingStart | processingEnd: the handlers finish | The page's own event handlers doing a lot of work. |
| Presentation delay | processingEnd | The next frame: startTime + duration | The 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:
// 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 printsIts 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:
// 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 printsIts 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:
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:
// 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 printsKept 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?
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:
// 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:
// 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 printsCounting 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:
input delay processing presentation delay
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 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?
- 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
Sort each cause by the wait it makes longer.
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.
The same eight latencies came from a visit with 60 interactions. What does the code print?
// 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`);INP: 128 ms. With 60 interactions, one is skipped, so the 216 ms click no longer counts. INP is the second slowest.
Add to cart's handler is cut down to 30 ms. What does the second line print?
// 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.`);Its handler ran for 30.0 ms. Processing is processingEnd minus processingStart. The input delay doesn't change: the handler still starts 2.0 ms after the click.
How many interactions did Firefox's 16 ms observer report entries for?
6, of the 8 it counted. The first Like click was too quick: its first-input entry says 8 ms. Firefox counted the busy Like click too, but gave it no entry of 16 ms or more. The recording can't tell whether the click reached Firefox only after the long task ended.
How long was WebKit's slowest interaction?
224 ms: the busy Like click again. Like Chrome, WebKit made it wait behind the long task.
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?
processingStart. With no handlers, nothing runs between the two, so processing is 0 ms. In Chrome's recording, the mouseover entry of the first Like click shows it: both times are 597.8 ms.
The Event Timing standard leaves out some events because they're "continuous". Name one that a mouse fires.
Any of mousemove, pointermove, pointerrawupdate, wheel or drag. They fire again and again while you move. The standard says it doesn't yet know how to count and add up such events in a useful way, so it leaves them out.
Check your understanding
For each question, ask which events belong together, which wait took the time, and how many interactions there were.
Question 1 of 7What does the INP example print?
Read the code, then predict// 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.
Question 2 of 7What does the first line of the busy Like click example print?
Read the code, then predict// 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.
Question 3 of 7Why did a quick Like handler give a 216 ms click in Chrome?
Choose an answer to see the explanation.
Question 4 of 7A click fires pointerdown, mousedown, pointerup, mouseup and click. How many interactions is that?
Choose an answer to see the explanation.
Question 5 of 7An observer with the default durationThreshold never reports your 40 ms keydowns. Why?
Choose an answer to see the explanation.
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.
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
durationThresholdto 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:
- Event Timing, W3C.
- Interaction to Next Paint (INP), web.dev.
- Optimize Interaction to Next Paint, web.dev.
- Performance: interactionCount property, MDN.
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.