A list page's Interaction to Next Paint at the 75th percentile is 420 ms. The field breakdown averages roughly 40 ms input delay, 60 ms processing, and 320 ms waiting for the next paint. Which change moves the metric most?
Show the full answer Hide the answer
The deciding number
Interaction to Next Paint is the sum of three segments: input delay (the main thread was busy when the tap arrived), processing time (your event handler), and presentation delay (style recalculation, layout, paint and compositing before the first frame that reflects the interaction). It replaced First Input Delay as a Core Web Vital in March 2024, and the field thresholds are 200 ms or less for good and above 500 ms for poor, assessed at the 75th percentile of page views.
Here 320 of the 420 ms sits in presentation delay. The handler is not the problem; committing the resulting DOM is. Any fix aimed at script time has a ceiling of 100 ms and cannot reach a passing score.
Why shrinking the commit wins
Style and layout cost scale with the number of nodes affected and the depth of the tree being invalidated. A tap that re-renders 1,800 rows forces the engine through one large synchronous layout before it can paint. Two moves attack that directly:
- Render fewer nodes. Virtualise the list so only the visible window plus a small overscan exists in the DOM. Going from 1,800 rows to 30 removes most of the layout work rather than moving it.
- Paint in two steps. INP stops at the first paint that reflects the interaction. Apply the cheap visual
acknowledgement — the pressed state, the selected row, a skeleton — then yield (
await scheduler.yield(), or asetTimeoutof 0 where that is not available) and do the expensive re-render in the next task. The measured interaction ends at the first frame, so the metric reflects what the user saw, and the user did see a response.
This is not a trick on the metric. A 60 ms acknowledgement followed by a 300 ms update genuinely feels responsive in a way that a 420 ms freeze does not.
Why the other options fail
- Web worker. Style, layout and paint are main-thread-only; a worker cannot do any of the 320 ms. Workers are the right answer when processing time dominates — a 300 ms sort, filter or parse — which is not this breakdown. Moving 60 ms of handler work off-thread also adds two postMessage hops.
- Debounce. Debouncing reduces total work when a user taps repeatedly, and INP measures the latency of single interactions. Collapsing taps delays the first response, so the metric can get worse. It is the right fix for a scroll or keystroke handler firing dozens of times per second.
- Yield before the handler. This targets input delay, which is 40 ms here. Even eliminating it entirely leaves 380 ms. It is the right first move when input delay is the large segment, which usually means long tasks from third-party scripts or hydration overlapping the interaction.
When this is the wrong answer
If the 320 ms is not layout but a font swap or a large image decode triggered by the interaction, virtualising the list changes nothing. Confirm with a performance trace of a real interaction on a real device before rebuilding the list: the breakdown tells you which segment, and only a trace tells you which work inside it.