DEV Community

Cover image for INP Is Not Just a Faster FID
nosyos
nosyos

Posted on

INP Is Not Just a Faster FID

I once spent an afternoon cutting a click handler from 80ms down to 18ms. Clean, fast, properly debounced. The INP score didn't move.

The handler wasn't the problem. The browser couldn't even start the handler for 190ms after the click — a long task was running at exactly the wrong moment. All that optimization was irrelevant.

INP splits an interaction into three phases. Most React developers know one of them.


The three-phase model

Every INP interaction goes through:

  1. Input delay — the time from when the user interacts to when the browser can start processing the event. This is blocked by whatever is already running on the main thread.
  2. Processing time — the time your event handlers actually execute.
  3. Presentation delay — the time from when handlers finish to when the browser renders the visual update. This is where React's reconciliation and paint happen.

The total of these three is what INP measures for each interaction. The worst interaction in a session becomes the score.

Optimizing processing time — the only phase most developers think about — is the right move when processing time is actually the bottleneck. It often isn't.


Input delay is the one that surprises people

Input delay is not caused by your handler. It's caused by whatever was running on the main thread the moment the user clicked.

A React app rendering a large list after a search query completes. A useEffect running a synchronous calculation on new data. A timer callback that scheduled itself to run every few seconds and happens to fire during an interaction. Any of these can generate 100–300ms of input delay that has nothing to do with the click handler the user triggered.

The Chrome UX Report attribution for a slow INP event will tell you which phase is taking the most time. If input delay is more than 50ms, handler optimization is the wrong direction.

To see the breakdown in your own data:

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType !== 'event' || entry.duration < 200) continue;

    console.log({
      interaction: entry.name,
      inputDelay: entry.processingStart - entry.startTime,
      processingTime: entry.processingEnd - entry.processingStart,
      presentationDelay: entry.duration - (entry.processingEnd - entry.startTime),
    });
  }
}).observe({ type: 'event', durationThreshold: 100, buffered: true });
Enter fullscreen mode Exit fullscreen mode

Run this for a week on production. Look at the breakdown. A high inputDelay on a specific page tells you there's a long task running during that page's normal usage cycle — and that's the thing to fix, not the handler.


Breaking up the tasks that cause input delay

The fix for input delay is reducing the size of long tasks so the browser has gaps to process input.

scheduler.yield() is the cleanest way to do this. It pauses execution and lets the browser handle any pending input before continuing:

async function processLargeDataset(items: Item[]) {
  const results: Result[] = [];

  for (let i = 0; i < items.length; i++) {
    results.push(expensiveTransform(items[i]));

    // Every 50 items, yield to let the browser breathe
    if (i % 50 === 0) {
      await scheduler.yield();
    }
  }

  return results;
}
Enter fullscreen mode Exit fullscreen mode

Without the yield, a 1,000-item dataset processed in one shot becomes a multi-hundred-millisecond long task that blocks input. With it, the browser gets a chance to handle clicks between chunks. INP stays low even while the work is ongoing.

scheduler.yield() is available in Chrome and Edge. For broader support, setTimeout(0) works as a fallback, though the browser may batch it less aggressively.


Presentation delay and the React render cost

Presentation delay is the third phase — from when handlers finish to when the screen actually updates. This is React's territory.

A click handler that calls setState and returns immediately still has to wait for React to reconcile and the browser to paint before the interaction is complete from INP's perspective. If reconciliation is expensive, presentation delay climbs.

This is where useTransition belongs — not as a general "make things faster" tool, but specifically to defer reconciliation work that doesn't need to block the visual acknowledgment of an interaction. The handler returns quickly, the browser paints a loading state or an immediate visual change, and then React reconciles the heavier update separately.


Reading LoAF attribution in production

The Long Animation Frames API — covered briefly in the previous article — becomes genuinely useful when you start reading its scripts attribution in a production context.

Each LoAF entry includes a list of scripts that contributed to the slow frame, with source locations and durations:

new PerformanceObserver((list) => {
  for (const frame of list.getEntries()) {
    if (frame.duration < 100) continue;

    for (const script of frame.scripts) {
      sendMetric({
        page: location.pathname,
        frameDuration: frame.duration,
        scriptSource: script.sourceURL,
        scriptFunction: script.sourceFunctionName,
        scriptDuration: script.duration,
        invokerType: script.invokerType, // 'event-listener', 'user-callback', etc.
      });
    }
  }
}).observe({ type: 'long-animation-frame', buffered: true });
Enter fullscreen mode Exit fullscreen mode

invokerType tells you whether the script was triggered by an event listener, a setTimeout, a Promise callback, or something else. Filtering by invokerType: 'event-listener' on a specific page shows you exactly which handlers are contributing to slow frames during interactions — with function names and source URLs.

This replaces a lot of the guesswork involved in reproducing INP issues locally. Slow interactions often don't reproduce in DevTools because the lab environment doesn't have the same background tasks, cache state, and concurrent timers as production.


Where to look first

Check inputDelay in the event timing breakdown before touching handler code. If input delay is the dominant phase, look for long tasks running during the page's usage cycle — not just during load.

If presentation delay is high on a specific interaction, that component's reconciliation cost is the target. Start with React DevTools Profiler on that specific action before generalizing.

Processing time being the bottleneck is the most straightforward case — and also the least common in practice.

Top comments (0)