AldeaCode Logo
Performance How to Fix INP Score: Interaction to Next Paint Guide
Performance AldeaCode Architecture

How to Fix INP Score: Interaction to Next Paint Guide

Diagnose and fix a poor INP score. What the metric measures, how scheduler.yield() helps, and how to track INP with the web-vitals library.

Why FID got replaced

For years we optimized for First Input Delay. The problem with FID is that it only looked at the first interaction on a page, and only at the part where the browser was waiting to start. Once your code started running, FID stopped counting. A page could score great on FID and still feel slow to actually use.

INP, Interaction to Next Paint, fixes that. It measures every click, tap, and key press on the page, and it counts the full round trip: from the moment the user touches the screen until the browser shows a new frame. Click a menu, wait 500ms for it to open because the main thread is busy, your INP reflects that.

What happens during one interaction

When a user clicks something, three things happen in order:

  1. Input delay: the wait before your code can even start. The main thread is busy doing something else (heavy JavaScript, hydration, a third party script) and the click sits in a queue.
  2. Processing time: your event handler runs. This is your code: a state update, a fetch, a calculation.
  3. Presentation delay: the browser recalculates layout, paints, and shows the new frame.

INP is the sum of all three. If any one of them is slow, the interaction feels slow.

Yielding to the main thread

The main rule for good INP is simple: do not block the main thread for long. If you have a chunk of work that takes 200ms, the browser cannot respond to clicks during those 200ms.

The classic trick was setTimeout(fn, 0) to break a long task into smaller pieces. It works, but it sends your task to the back of a long queue, and other things can sneak in.

scheduler.yield() is the new native API for this. It hands control back to the browser so it can paint or handle a pending click, then resumes your code right after. It is meant for exactly this use case.

async function processLargeBatch() {
  for (const item of giantList) {
    calculate(item);

    // Every so often, let the browser breathe
    if (shouldYield()) {
      await scheduler.yield();
    }
  }
}

Use it inside loops that process a lot of items, or anywhere you have a chunk of synchronous work that does not need to finish all at once.

Lighthouse will lie to you about INP

INP is a field metric. It only makes sense with real users on real devices. Lighthouse runs in a controlled lab environment, so a page can score 100 in Lighthouse and still have terrible INP for actual visitors on a mid-range Android.

The fix is to measure in production. The web-vitals library has an attribution build that tells you not just the INP value, but which interaction caused it and where the time went.

import { onINP } from 'web-vitals/attribution';

onINP((attribution) => {
  console.log('Interaction:', attribution.interactionType); // 'pointerdown', 'keydown'
  console.log('Target Element:', attribution.processedEventTarget);
});

Send this to your analytics. Once you can see which button on which page is slow, the fix is usually obvious.

Practical advice

Avoid long tasks. Anything over 50ms on the main thread is a problem. Defer work that does not need to happen right now: analytics, prefetching, non-critical hydration. Watch out for hydration in particular, it is a common cause of bad INP on the first interaction after a page loads. If you have heavy computation, look at WebAssembly to move it off the main thread, and at efficient programming patterns to keep your overall workload small.

If you want a tool that flags this kind of issue automatically, our SEO Expert includes INP diagnostics.

Frequently Asked Questions

What we do

Honest sites. No shortcuts.

Real engineering, careful design. Liked the post? Let's talk about your project.

Get in touch →

You might also like

Browse all articles →