Skip to content

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.

PhaseWhat it isCaused by
Input delayBetween the tap and the handler runningThe main thread was busy with something else
ProcessingYour event handlers executingYour code — usually a state update cascade
Presentation delayHandler finished → pixels changedStyle, layout, paint, composite for the update
Most teams assume they have a processing problem. Input delay and presentation delay are at least as common, and they have completely different fixes.

You can read the split directly:

js
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.

  1. Find long tasks and attribute them

    A task over 50ms blocks input for its remainder. PerformanceLongAnimationFrame gives you the script that caused it, which the older long-task API did not.

  2. 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.

  3. 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.

js
// 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.

  1. 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. useDeferredValue expresses exactly that, and it is the single most effective INP fix in a React application.

  2. 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.

  3. Memoise the expensive derivation, not every component

    Wrapping everything in memo adds comparison cost everywhere and usually helps nowhere. Profile, find the one derivation that is genuinely expensive, memoise that.

tsx
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:

  1. 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.

  2. Enormous DOM subtrees re-styling

    A class toggled high in the tree re-styles everything below it. contain: layout style on independent sections bounds that work.

  3. Off-screen work that is not off the critical path

    content-visibility: auto lets 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:

OptimisationWhat it actually improves
Image compressionLCP and bandwidth. Not interaction.
Smaller CSSFirst paint. Interaction only if selectors are pathological.
A CDNTTFB. The main thread is unaffected.
Code splitting a routeInitial load. May make INP worse if a chunk loads on interaction.
Preloading fontsLCP and CLS.
Server-side renderingFirst paint. Hydration can make input delay worse.
Every one of these is sometimes worth doing. None of them is an INP fix.

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.

InteractionWithout JavaScript
Accordion / disclosure<details> — also in the tab order, and find-in-page opens it
Modal dialog<dialog> with showModal(); focus trap included
Popover / dropdownThe Popover API
TabsRadio inputs plus :checked selectors
Scroll-linked animationanimation-timeline: view() or scroll()
Sticky / pinned sectionsposition: sticky
Each of these is a real interaction with an INP of essentially zero, because no script runs.

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

  1. 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.

  2. Attribute by target, and fix the worst control

    INP reports your worst interaction. Improving the average changes nothing.

  3. Throttle the CPU when profiling locally

    4× or 6× slowdown. Without it you will conclude everything is fine, because on your machine it is.

  4. 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

  1. Instrument the three-phase split and find your worst target

    Everything else is guesswork until you know which phase you are losing.

  2. Ask whether that interaction needs JavaScript at all

    The cheapest fix by a wide margin.

  3. If it does: defer the non-urgent part of the update

    useDeferredValue for anything derived from typing or dragging.

  4. Check you are animating transform and opacity only

    Presentation delay is invisible in a handler profile and frequently dominates.

  5. 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.