Writing
Interaction to Next Paint: what actually moves it, and what only looks like it does
INP measures three things and most advice only addresses one of them. A breakdown of input delay, processing and presentation delay — with the fixes that move each, and the popular optimisations that move none.
6 min read
INP replaced First Input Delay because FID measured the easy part. A page could score beautifully on FID and still feel broken, because FID stopped counting at the moment the handler began — not when anything appeared on screen.
INP measures the whole interaction, and that makes it much harder to game and much more useful. It also means a lot of common advice does nothing for it.
The three parts, and which one you are actually losing
Every interaction decomposes into three phases. Measure them separately or you will optimise the wrong one.
| Phase | What it is | Caused by |
|---|---|---|
| Input delay | Between the tap and the handler running | The main thread was busy with something else |
| Processing | Your event handlers executing | Your code — usually a state update cascade |
| Presentation delay | Handler finished → pixels changed | Style, layout, paint, composite for the update |
You can read the split directly:
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const inputDelay = entry.processingStart - entry.startTime;
const processing = entry.processingEnd - entry.processingStart;
const presentation = entry.startTime + entry.duration - entry.processingEnd;
console.log({ inputDelay, processing, presentation, target: entry.target });
}
}).observe({ type: 'event', durationThreshold: 40, buffered: true });Input delay: the main thread was busy before you arrived
This one has nothing to do with your handler. Something else was running when the user tapped.
Find long tasks and attribute them
A task over 50ms blocks input for its remainder.
PerformanceLongAnimationFramegives you the script that caused it, which the older long-task API did not.Break up anything that runs after load
Hydration, analytics initialisation, a third-party widget booting. Yield between chunks of work rather than running to completion.
Third-party scripts are usually the largest single cause
And the one nobody profiles, because it is not in your bundle. Load them with
async, and audit what they do on the main thread after they arrive.
// Yield to the browser so a pending input can be serviced.
async function processAll(items) {
for (const item of items) {
doWork(item);
if (navigator.scheduling?.isInputPending?.()) await scheduler.yield();
}
}Processing: the update cascade, not the handler
The handler itself is rarely slow. What is slow is everything React does because of it.
Separate urgent from non-urgent updates
The input's own value must be immediate. The filtered list of 400 results does not have to be.
useDeferredValueexpresses exactly that, and it is the single most effective INP fix in a React application.Do not re-render a tree to change a leaf
State lifted to a page component re-renders the page. State that lives where it is used re-renders that.
Memoise the expensive derivation, not every component
Wrapping everything in
memoadds comparison cost everywhere and usually helps nowhere. Profile, find the one derivation that is genuinely expensive, memoise that.
const [query, setQuery] = useState('');
const deferred = useDeferredValue(query); // lags behind, on purpose
const results = useMemo(() => search(corpus, deferred), [corpus, deferred]);
// The input paints on the next frame. The list catches up when it can.
<input value={query} onChange={(e) => setQuery(e.target.value)} />Presentation delay: the phase nobody looks at
The handler finished, the state changed, and the frame still has not arrived. This is style, layout, paint and composite for the update — and it is where a fast handler still produces a slow interaction.
Three more that show up repeatedly:
Layout thrashing in the handler
Read a geometry property, write a style, read again — each read after a write forces a synchronous layout. Batch all reads, then all writes.
Enormous DOM subtrees re-styling
A class toggled high in the tree re-styles everything below it.
contain: layout styleon independent sections bounds that work.Off-screen work that is not off the critical path
content-visibility: autolets the browser skip rendering work for content that is not visible, which is exactly the work that delays a frame elsewhere.
What does not help
Worth naming, because these consume real effort and move the metric very little:
| Optimisation | What it actually improves |
|---|---|
| Image compression | LCP and bandwidth. Not interaction. |
| Smaller CSS | First paint. Interaction only if selectors are pathological. |
| A CDN | TTFB. The main thread is unaffected. |
| Code splitting a route | Initial load. May make INP worse if a chunk loads on interaction. |
| Preloading fonts | LCP and CLS. |
| Server-side rendering | First paint. Hydration can make input delay worse. |
The structural fix: interactions that need no JavaScript
The fastest interaction is one with no handler, and modern CSS covers more of them than most teams realise.
| Interaction | Without JavaScript |
|---|---|
| Accordion / disclosure | <details> — also in the tab order, and find-in-page opens it |
| Modal dialog | <dialog> with showModal(); focus trap included |
| Popover / dropdown | The Popover API |
| Tabs | Radio inputs plus :checked selectors |
| Scroll-linked animation | animation-timeline: view() or scroll() |
| Sticky / pinned sections | position: sticky |
On this site an animation library was removed entirely — it cost 45 kB for one section, and
every part of it turned out to be position: sticky plus a CSS scroll-driven animation. Those
interactions cannot have an INP problem, because there is nothing to execute.
Measuring it honestly
Field data first, lab data second
INP is a p75 across real interactions on real devices. A lab profile on a fast laptop will not reproduce it — the interactions that fail are usually on mid-range Android.
Attribute by target, and fix the worst control
INP reports your worst interaction. Improving the average changes nothing.
Throttle the CPU when profiling locally
4× or 6× slowdown. Without it you will conclude everything is fine, because on your machine it is.
Record the conditions beside the number
Device class, throttling, build. A figure without a method cannot be compared to itself next month, and is not evidence of an improvement.
The order to work in
Instrument the three-phase split and find your worst target
Everything else is guesswork until you know which phase you are losing.
Ask whether that interaction needs JavaScript at all
The cheapest fix by a wide margin.
If it does: defer the non-urgent part of the update
useDeferredValuefor anything derived from typing or dragging.Check you are animating transform and opacity only
Presentation delay is invisible in a handler profile and frequently dominates.
Then go after long tasks and third-party scripts
Input delay is somebody else's code as often as it is yours.
Start here
Tell us what you are building
Or what is breaking, or what has to go faster. You will get a straight answer from an engineer who would do the work.