What you'll learn: By the end of this lesson you will be able to explain what an interaction is and why its latency is the longest event duration, compute INP from a list of recorded event entries (including the "ignore one highest per 50" rule and the 75th percentile across page views), read the three phases of an interaction, choose a fix for each phase, and explain why the default Event Timing threshold can hide interactions from your own tooling.
You press the call button of a lift. You do not judge the lift by how fast it arrives. You judge the button by how soon its light comes on. A button that lights after three seconds feels broken, even if the lift is quick. A web page has the same contract: after a click, tap or key press, show something changed, and show it soon. Interaction to Next Paint (INP) measures how long that takes, at the worst point of a visit.
The Core Web Vitals lesson introduced INP next to LCP and CLS, and the event loop lesson explained why long tasks delay input. This lesson is the dedicated treatment: how the number is built, so you can predict it, compute it and move it.
The running example is a product page with a size selector, a search box and an "Add to cart" button whose click handler re-renders a large part of the page. The recordings in this lesson are hand-written and illustrative, not captured from a real page.
The core idea
Three statements carry the lesson.
- INP measures the wait for the next frame after an interaction, not the time your handler takes. The clock starts when the user acts and stops when the browser paints the next frame (web.dev: "the single longest duration of a group of event handlers", measured until the browser can next paint).
- An interaction is a group of events, and its latency is the longest one in the group. web.dev defines an interaction as "a group of event handlers that fire during the same logical user gesture", and says the event with the longest duration within it contributes the interaction's latency. Durations are not added.
- The page's INP is the worst interaction of the visit, with outlier forgiveness. On long visits one highest interaction is ignored for every 50.
From intuition to internals
Events become entries
The Event Timing API gives each qualifying event an entry with fields the calculator needs. MDN describes processingStart - startTime as the time before the handler started, processingEnd - processingStart as the handler's run time, and duration as the time from startTime to the next rendering paint, rounded to the nearest 8 ms. The specification says rounding is to the nearest 8 ms, with a granularity of 8 ms or less.
Each entry also has an interactionId. Per the specification it is 0 unless the event is a pointerdown, pointerup or click of a tap or drag, or a keydown or keyup of a key press; "pointerdown that ends in scroll is excluded". Hovering and scrolling are not interactions, which matches web.dev: INP observes clicks, taps and key presses. A pointerup gets a new id that is set on both the pointerdown and the pointerup, and the matching click reuses it. A keydown starts a new id and the keyup must match a previous keydown.
Figure 1. From a tap to a number
How to read this: the browser records timing, a script groups it and picks the value, and the value travels to a store that reports the 75th percentile. web.dev says the page's INP is calculated when the user leaves the page, and recommends reporting whenever the page goes to the background too.
One interaction, three phases
web.dev splits the latency of the longest event into input delay (until callbacks start), processing duration (all callbacks) and presentation delay (from the end of callbacks until the frame is presented). The first is often work already running on the thread, the second is your handlers, the third is style, layout and paint.
Figure 2. A tap, event by event
How to read this: three events, one id. Each has its own duration. The interaction's latency is the longest of them, and here that is the click.
From interactions to one number
For a page view: collect interactions, sort their latencies high to low, skip one for every 50 interactions, and report the next. With fewer than 50 interactions the worst one is reported. web.dev's JavaScript guidance says to take the 98th percentile of the interactions on unload. The lab checks that, under a nearest-rank percentile, "skip floor(n / 50) highest" and "98th percentile" are the same number for every n from 1 to 1000.
Across page views, thresholds apply at the 75th percentile of page loads, segmented by device class: 200 ms or less is good, above 200 up to 500 is needs improvement, above 500 is poor. A page where nobody clicked, tapped or typed has no INP value.
Figure 3. Computing INP
duration in the group"] D --> E["Sort high to low"] E --> F["Skip floor(n / 50)"] F --> G["Next one = INP of this page view"] G --> H["p75 over page views,
rated 200 / 500 ms"]
How to read this: nothing is added. Grouping takes the longest event; the page value takes the worst interaction after forgiveness; the dashboard takes the 75th percentile of those.
Three illustrations
1. A minimal example: one tap
A tap produces pointerdown (16 ms), pointerup (16 ms) and click (64 ms) with one id. The interaction's latency is 64 ms, not 96. The calculator groups them and names the click as the longest event.
2. A realistic example: the product page session
The recording has 12 entries and 4 interactions. The lab's output shows what each phase contributed:
| Interaction | Events | Latency | Input delay | Processing | Presentation |
|---|---|---|---|---|---|
| 101 size tap | pointerdown, pointerup, click | 64 | 4 | 36 | 24 |
| 102 key press | keydown, keyup | 56 | 10 | 20 | 26 |
| 103 key press | keydown, keyup | 128 | 30 | 70 | 28 |
| 104 Add to cart | pointerdown, pointerup, click | 344 | 4 | 216 | 124 |
The INP is 344 ms, "needs improvement". The worst interaction has a small input delay (4 ms), so yielding will not help it. Processing (216 ms) and presentation (124 ms) dominate, so the fix is in the handler and the render it triggers. The 128 ms key press is a different story: 30 ms of input delay suggests the thread was busy when the key arrived, the case slicing and priorities (see Scheduling Work) address. The presentation figure is derived, not measured, because the browser rounds duration.
3. A failure case: the tool that cannot see the problem
The Event Timing API exposes entries whose duration is at least a threshold. The specification's default is 104 ms, and a requested threshold below 16 is treated as 16. A session whose worst interaction takes 96 ms is invisible at the default:
durationThreshold 104: 0 of 5 entries observed, INP none reported
durationThreshold 16: 5 of 5 entries observed, INP 96 ms
A homemade collector that leaves the default threshold in place reports "no INP" for fast pages and drops the shorter interactions on slow ones, so its interaction count is lower than the real one. Lower the threshold deliberately, or use a library whose reference implementation web.dev points to, and check its source for what it observes.
Fixing each phase
Figure 4. Which phase is large?
slice and yield"] P -->|"processing"| B["Do less in the handler;
defer the rest"] P -->|"presentation"| C["Reduce style, layout
and render work"]
How to read this: measure first, then spend effort where the time is. A faster handler does nothing for a click that waited behind a long task.
web.dev's optimization guide gives the working rules. Start from field data, then reproduce in the lab. Do as little as possible in event callbacks, and run only what the next frame needs before it: its example defers the other work with setTimeout inside requestAnimationFrame(). It states that yielding indiscriminately is better than not yielding at all, and defines layout thrashing as a rendering performance problem where layout occurs synchronously, a common cause of a large presentation delay.
Build it: compute INP from a recording
Goal. Group recorded event entries into interactions, compute INP and its rating, and see the threshold and outlier rules act on data.
Setup. The lab is examples/frontend/labs/inp-calculator. It needs Node.js (captured on Node 26.12.1).
cd examples/frontend
pnpm install
cd labs/inp-calculator
pnpm test # 24 tests
node src/demo.ts
Steps.
- Open
src/recordings.ts. The product page session has 12 entries; two (mouseover, a scrollpointerdown) haveinteractionId0. - Run the demo and match each row of section 1 to the table above. Confirm that interaction 101 is 64 ms, not 96.
- Read section 2: the same session set read at thresholds 104 and 16.
- In section 3, find the 120-interaction session. The worst is 640 ms, the reported INP is 296 ms, because
floor(120 / 50) = 2interactions are ignored. - In section 4, the eight page views have a 75th percentile of 232 ms, while the single worst is 720 ms.
Expected output, captured from a real run (Node 26.12.1, 2026-11-08):
=== 1. productPageSession: events grouped into interactions
recorded entries: 12, with interactionId > 0: 10
id start events latency from input proc present
101 2000 pointerdown+pointerup+click 64 click 4 36 24
102 5000 keydown+keyup 56 keydown 10 20 26
103 5200 keydown+keyup 128 keydown 30 70 28
104 8000 pointerdown+pointerup+click 344 click 4 216 124
interactions: 4, ignored: 0, INP: 344 ms (needs improvement)
=== 2. The 104 ms default threshold hides small interactions
durationThreshold 104: 0 of 5 entries observed, INP none reported
durationThreshold 16: 5 of 5 entries observed, INP 96 ms
=== 3. longSession: 120 interactions, one highest ignored per 50
interactions: 120, ignored: 2
five highest latencies: 640, 504, 296, 112, 112 ms
worst interaction: 640 ms (poor); reported INP: 296 ms (needs improvement)
=== 4. Field value: the 75th percentile of page views
page-view INPs: 96, 120, 144, 168, 184, 232, 480, 720
p75: 232 ms (needs improvement)
for comparison, the single worst page view: 720 ms (poor)
This is a model of a published rule, not a browser. The data is hand-written. The lab did not read the web-vitals library's code, so it cannot say what that library keeps for very long sessions, and the 75th percentile uses a nearest-rank definition that may differ from a vendor's.
Break it. In groupInteractions, make the latency the sum of the group's durations. The "does not add the durations" test and the product page test fail. Revert it.
Stretch. Add the keyboard composition rules from the Event Timing specification, or compare with the web-vitals source.
In practice: an "Add to cart" button that feels slow
This is a realistic scenario, not a case history. It is a composite built from common production patterns. It describes no specific company or product, and every number in it is an illustrative assumption.
The situation
A store's product page has a field INP at the 75th percentile above 200 ms, and the worst interaction in session recordings is almost always the "Add to cart" click. Four engineers own the page.
Starting architecture
The click handler updates a cart store, recomputes prices for the whole list, rebuilds the cart drawer DOM and logs analytics synchronously. In the product page recording: 4 ms of input delay, 216 ms of processing, 124 ms of presentation delay.
Options considered
| Option | What it fixes | What it costs | Risk |
|---|---|---|---|
| Yield before the handler's work | Nothing here: input delay is small | Complexity | Wasted effort |
| Do the visible update first, defer prices and analytics until after the next paint | Processing time | Order of effects must be explicit | A bug if deferred work is skipped |
| Limit the render: update only the cart badge and a small drawer | Presentation delay | Component changes | Stale UI if the update is incomplete |
| Move the price calculation to a worker | Processing time | Message plumbing | Result still lands on the main thread |
The decision
| Field | Decision record |
|---|---|
| Context | Worst interaction is a click with large processing and presentation phases. |
| Decision | Update the badge and drawer first; defer price recalculation and analytics to a later task; render only what changed. |
| Consequences, good | The next frame shows a response sooner; each phase shrinks. |
| Consequences, bad | Prices update a moment later; more explicit ordering code. |
| Revisit when | Field p75 stays above 200 ms or a new worst interaction appears with a large input delay. |
Rollout and verification
- Reproduce with CPU throttling and read the main-thread timeline for the click.
- Ship behind a flag; compare p75 INP for flagged and unflagged page views.
- Check that the deferred work still runs when the user leaves the page quickly.
The 2 a.m. failure
Two weeks later, INP gets worse on every page, not just the product page. A consent tag added a document-level click listener that runs synchronous work for every click, and page-level fixes cannot offset it.
First three checks: (1) in a throttled profile, look for a handler on the document that runs on every click; (2) compare processing duration across pages: a constant addition points to a shared listener; (3) disable the tag in a test and confirm INP recovers. Mitigation: disable the tag or load it after interaction handling. Follow-up: budget third-party handler time and add a field alert on processing duration, not only on INP.
Trade-offs
| Choice | Gains | Costs | Use when |
|---|---|---|---|
| Field data first | What real users feel | Slow feedback; needs reporting | Always, before tuning |
| Lab reproduction | Debuggable, repeatable | Not the same as users' devices | After field data shows slow interactions |
| Defer non-critical work | Faster next frame | Ordering complexity | Handlers with visible and invisible work |
| Yield inside handlers | Shorter tasks | Async code | Long handler work that must stay on the main thread |
| Lower observer threshold | Complete data | More entries to process | Any homemade collector |
Architect's notes
- Set a target at the 75th percentile, by device class, and alert on it.
- Track the three phases, not only the total, so alerts say where to look.
- Treat third-party listeners as part of your handler time.
- Prefer a maintained library for collection, and read its source for thresholds.
Common mistakes
- Adding event durations together. The interaction's latency is the longest event.
- Thinking INP is handler time. It runs to the next paint.
- Using the average. The reported value is a worst-case with forgiveness, summarised at the 75th percentile.
- Leaving the default duration threshold in a custom collector.
- Optimizing the handler when input delay is the problem, or the reverse.
Key takeaways
- An interaction is a group of events; its latency is the longest event duration in the group.
- INP is the worst interaction of a page view, ignoring one highest per 50; fields are judged at the 75th percentile.
- Good is 200 ms or less; poor is above 500 ms.
- Input delay, processing and presentation delay need different fixes.
- No click, tap or key press means no INP value.
- The Event Timing default threshold is 104 ms; lower it for your own collection.
Practice exercises
Exercise 1, Starter (30 min): Add a keydown of 72 ms and a keyup of 24 ms with one id to the product page session. Predict the new interaction and INP, then run the demo.
Exercise 2, Intermediate (2 hours): Write a recording of 99 interactions and one of 100 with the same slowest interaction, and show how the INP differs.
Exercise 3, Advanced (half day): Compare the lab's p75 with another percentile method on a list where the rating changes, and write when it matters.
Related reading
In this course:
- Loading Performance and Core Web Vitals: the three vitals and field measurement.
- The Event Loop and the Main Thread: long tasks and the simple yield.
- Scheduling Work: Yielding, Idle Time and Task Priorities: slicing and priorities for input delay.
Official documentation
- web.dev: Interaction to Next Paint (INP): definition, phases, calculation and thresholds.
- web.dev: Optimize Interaction to Next Paint: field then lab, yielding, deferring and layout thrashing.
- W3C: Event Timing API:
interactionId,durationand the thresholds. - MDN: PerformanceEventTiming: the fields and the event list.
- web.dev: Optimize long tasks: the 50 ms long task and yielding.