Skip to content

    Frontend System Design

    Interaction to Next Paint and Responsiveness

    Learn how INP groups events into interactions, which one is reported, how to read its three phases, and how to compute and improve it from recorded data.

    By Published 12 min read

    • frontend-system-design
    • inp
    • core-web-vitals
    • responsiveness
    Share

    Standard lessonadvancedTypeScript labFrontend System Design

    Before this: Loading Performance and Core Web Vitals, Scheduling Work: Yielding, Idle Time and Task Priorities

    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.

    1. 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).
    2. 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.
    3. 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 an interaction becomes an INP value in the fieldUser tap to Browser; Browser to Page script (entries); Page script to Report (INP value); Report to Analytics storeUser tap: pointerdown, pointerup, clickUser tappointerdown,pointerup, clickBrowser: Event Timing entriesBrowserEvent TimingentriesPage script: observer or web-vitalsPage scriptobserver orweb-vitalsReport: on hide or unloadReporton hide or unloadAnalytics store: p75 per pageAnalyticsstorep75 per pageentriesINP value

    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

    sequenceDiagram accTitle: Events and phases of one tap accDescr: A tap produces pointerdown, pointerup and click with one interaction id. The click waits for the main thread, runs its handler, then the browser paints the next frame. Its duration is the interaction latency. participant U as User participant M as Main thread participant P as Paint U->>M: pointerdown (id 104) U->>M: pointerup (id 104) U->>M: click (id 104) Note over M: input delay (thread busy?) M->>M: handler runs (processing) M->>P: render the next frame Note over P: presentation delay, then paint P-->>U: new pixels (end of duration)

    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

    flowchart TD accTitle: How INP is computed for one page view accDescr: Entries with interactionId above zero are grouped by id, each group takes its longest duration, latencies are sorted high to low, floor of n over 50 are skipped, the next one is the INP, and values from many page views are summarised at the 75th percentile. A["Event entries"] --> B{"interactionId > 0?"} B -->|"no"| X["Ignore"] B -->|"yes"| C["Group by interactionId"] C --> D["Latency = longest
    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?

    flowchart TD accTitle: Choosing a fix by INP phase accDescr: Look at the longest event of the worst interaction. If input delay is large, find the long task that was running and slice it. If processing is large, do less in the handler and defer the rest until after the next paint. If presentation delay is large, reduce style, layout and rendering work. S["Worst interaction"] --> P{"Largest phase?"} P -->|"input delay"| A["Find the busy task,
    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.

    1. Open src/recordings.ts. The product page session has 12 entries; two (mouseover, a scroll pointerdown) have interactionId 0.
    2. Run the demo and match each row of section 1 to the table above. Confirm that interaction 101 is 64 ms, not 96.
    3. Read section 2: the same session set read at thresholds 104 and 16.
    4. In section 3, find the 120-interaction session. The worst is 640 ms, the reported INP is 296 ms, because floor(120 / 50) = 2 interactions are ignored.
    5. 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

    NOTE

    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

    1. Reproduce with CPU throttling and read the main-thread timeline for the click.
    2. Ship behind a flag; compare p75 INP for flagged and unflagged page views.
    3. 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.

    In this course:

    Official documentation

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments