Frontend Inbox

INP is a main-thread problem

Intermediate9 minUpdated 2026-09-03

Why Interaction to Next Paint punishes long tasks rather than slow servers, how to find the blocking work in a trace, and the four fixes that move the number.

#performance
#metrics

In one sentence

Interaction to Next Paint measures how long the main thread makes a user wait after they touch something — so it is fixed by doing less work, not by faster servers.

Why it matters

INP replaced First Input Delay as a Core Web Vital because FID only measured the delay before a handler started. That flattered almost every site: input delay is usually a few milliseconds, while the handler itself and the render it triggers are where the seconds go.

INP measures the whole thing, end to end:

PhaseWhat it coversTypical culprit
Input delayWaiting for the main thread to be freeA long task already running
ProcessingYour event handlersSynchronous state updates, JSON.parse
PresentationStyle, layout, paint, compositeLayout thrash, a huge re-render

The score is not an average. The browser records every interaction and reports roughly the worst one (the 98th percentile once a page has enough interactions). One bad modal open ruins the page's number even if a hundred button clicks were instant.

Thresholds, for reference:

  • Good: 200 ms or less
  • Needs improvement: 200–500 ms
  • Poor: over 500 ms

Finding the blocking work

Field data tells you that you have a problem; a local trace tells you where. Start with the field, because the lab will not reproduce a slow Android phone on your laptop.

// Log the worst interactions from real users, with the element that caused them
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration < 200) continue
    console.warn('slow interaction', {
      duration: Math.round(entry.duration),
      name: entry.name, // 'pointerdown', 'keydown', …
      target: (entry as PerformanceEventTiming).target?.nodeName,
    })
  }
}).observe({ type: 'event', durationThreshold: 200, buffered: true })

Then reproduce it in DevTools with 4× CPU throttling on, record a performance trace, and click the offending control. You are looking for a single long yellow block. Anything over 50 ms is a long task; anything over 200 ms is your INP.

The four fixes that actually move the number

1. Yield between chunks of work. The most common shape is a handler that does everything before the browser gets a chance to paint. Split it: do the visible part first, yield, then finish.

async function onFilterChange(value: string) {
  setQuery(value) // cheap, paints immediately
  await new Promise((resolve) => setTimeout(resolve, 0))
  setResults(expensiveFilter(value)) // the slow part, after a paint
}

scheduler.yield() is the purpose-built version of that setTimeout where it is available, and unlike setTimeout it keeps your continuation at the front of the queue rather than the back.

2. Separate the urgent update from the expensive one. Rendering a large list in response to typing is the canonical INP failure. In React, useDeferredValue lets the input update at full speed while the list catches up.

const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)

// Input paints on every keystroke; the 5,000-row list re-renders when it can.
return (
  <>
    <input value={query} onChange={(e) => setQuery(e.target.value)} />
    <Results query={deferredQuery} />
  </>
)

3. Stop reading layout inside handlers. offsetHeight, getBoundingClientRect, and friends force a synchronous layout. In a loop that also writes styles, you get layout thrash — one forced reflow per iteration. Read everything first, then write everything.

4. Ship less JavaScript to the interaction. A component that only matters after a click does not need to be in the initial bundle, and a third-party script that runs analytics on every pointerdown is competing with your handler for the same thread.

Common pitfalls

  • Optimising the wrong percentile. A median of 90 ms with a p98 of 900 ms is a poor INP. Chase the tail.
  • Testing on the machine you wrote the code on. A desktop CPU hides almost every INP problem. Throttle, or you will conclude the metric is broken.
  • Debouncing the handler instead of the work. Debouncing input delays the visible feedback too, which the user experiences as the same lag with extra steps.
  • Blaming the network. INP has no network phase. If the number is bad, the work is on the main thread.

Further reading