Performance

INP: What Actually Moves It, and Why Your Handler Is Only One Third of the Story

INP is the metric people optimise by rewriting event handlers, which is roughly one third of the job. The other two thirds happen at points in the timeline that have nothing to do with your handler code.

The reason is in the definition, and once you see the three phases it becomes obvious where the rest of the time goes.

The metric, stated properly

INP — Interaction to Next Paint — measures the latency of interactions over the life of the page. The value reported is the longest interaction, and the thresholds are:

RatingINP
Good≤ 200 ms
Needs improvement200–500 ms
Poor> 500 ms

It is assessed at the 75th percentile of page loads, segmented separately for mobile and desktop — the same standard as the other Core Web Vitals.

One detail that matters on highly interactive pages: for pages with many interactions, the reported figure is not simply the worst one. A high percentile is used rather than the maximum, so a single outlier interaction on a busy page does not define the score. That is a deliberate design choice, and it means “fix the single worst interaction” is not always the right strategy.

The three phases

INP decomposes into three segments, and the documentation is explicit that they sum to the total: “The sum of these three phases is the total interaction latency.”

PhaseWhat it covers
Input delayFrom the user’s input to the point your event callbacks begin
Processing timeFrom the callbacks starting to them completing
Presentation delayFrom completion to the next frame containing the result being painted

Only the middle phase is your handler. The first and third are governed by what the main thread was already doing.

Input delay: the phase that punishes unrelated work

Input delay is the gap between the user pressing a key or tapping, and your handler getting to run. That gap exists because the main thread is busy.

If a long task is executing when the user taps — a third-party script parsing, a large data transformation, a timer-driven job — the browser cannot dispatch the event until the main thread yields. The user’s tap is queued behind work that has nothing to do with their tap.

This is the mechanism most articles miss. The conclusion to draw is not “my handler is slow,” it is: any long task on the main thread inflates INP on the next interaction, regardless of what that task does.

You can see it in the numbers. The event timing specification defines an interaction’s duration as running from the event’s timestamp to the next rendering update after the callbacks complete. Existing main-thread work sits between those two points, so it lands inside the measured duration.

The practical consequence: finding INP problems requires looking at what the main thread was doing when the user interacted, not just at the handler that ran. A performance profile during the interaction is the tool, and the answer is frequently a script you did not write.

Which interactions count

Not every event is measured. The specification is precise about this, and the exclusions are well chosen:

Counted: pointerdown, pointerup and click that belong to a tap or drag gesture, plus keydown and keyup belonging to a key press.

Explicitly excluded: pointerdown that ends in a scroll. Continuous events — mousemove, pointermove, wheel, drag — are exposed by the API but deliberately not counted, and MDN is direct that scrolling and zooming are not part of this metric.

So hovering a menu or scrolling a page does not add interactions to your INP. This is why a site with a parallax scroll effect can have excellent INP and a site with an expensive click handler cannot: the metric measures discrete input-to-paint latency, and continuous gestures are excluded by design.

Processing time: the part you control

This is the conventional optimisation target, and the documented causes are worth listing because they are not all obvious:

  • Handler workload — the obvious one
  • Long tasks triggered by or overlapping the handler
  • Layout thrashing — reading layout properties and then writing styles in a loop, forcing repeated reflow

Layout thrashing is the sneaky one. A handler that reads offsetHeight, then sets a style, then reads another geometry property, forces the browser to recompute layout on each read. The code looks correct; the cost is quadratic in the number of iterations.

The documented mitigation techniques are yielding and scheduling. The scheduler API’s yield() method is documented on MDN as yielding to the main thread and scheduling the continuation as a prioritized task, which lets the browser paint between chunks of work rather than after all of it. It is the modern tool for the problem older code solved with setTimeout.

Presentation delay: the phase with the largest DOM

Presentation delay is the time from your handler finishing to the next frame being painted. Two documented contributors:

  • A large DOM. Rendering cost scales with the number of elements, so a 10,000-node page pays for it on every paint.
  • JavaScript-rendered HTML that blocks yielding. If a framework re-renders a subtree synchronously inside the handler, the paint waits for that work.

The interesting property of presentation delay is that it is often the largest phase on sites where the handler itself is trivial — a click that toggles a class in 1 ms can still take 180 ms to appear if the paint is expensive.

That is the diagnostic value of the three-phase split. If input delay dominates, the problem is elsewhere on the thread. If processing dominates, it is your code. If presentation dominates, it is the DOM or the render.

Measuring it

Field data is the only thing that counts for the score, and it comes from real user monitoring. Lab tools can reproduce a problem but cannot tell you what your 75th percentile is.

To find the offending interactions, the useful approach is profiling a session where you interact the way a reader would: click a navigation control, open a menu, submit a form — and read the main-thread trace for long tasks overlapping those moments. The interaction is the marker; the trace tells you what was running when it happened.

A short plan

  1. Look for long tasks first. If input delay is a meaningful share of your INP, no amount of handler rewriting helps.
  2. Audit third-party scripts. They are the most common source of main-thread work you did not write and cannot see in your own codebase.
  3. Check for layout thrashing in any handler that reads geometry and writes styles.
  4. Yield inside long handlers rather than doing everything in one synchronous block.
  5. Watch the DOM size if presentation delay dominates.
  6. Do not optimise scroll or hover — they are excluded from the metric. Optimise them for users if they feel bad, but do not expect the score to move.

When INP is not your problem

If your site is a static content site with almost no JavaScript, you have very few interactions to measure and the metric is likely good by construction. This is a genuine advantage of building without a framework: a page that ships no client-side JS cannot have input delay caused by its own scripts.

Likewise, if your INP is comfortably under 200 ms, further work has no measured benefit. The 75th-percentile framing means you are optimising the experience of your slower quarter of sessions — worth doing when you are over the line, and not worth chasing when you are a long way under it.


Written by TestedHost. Every recommendation on this site comes from running the setup described, on a live deployment — not from a vendor spec sheet. Spotted something out of date? Tell us.