8 min read

Fixing INP in Next.js: Find the Slow Interaction, Then Break It Up

Interaction to Next Paint measures how long a page takes to visually respond after a tap, click or key press. Google rates 200ms or less as good. INP is the Core Web Vital that Lighthouse is least able to show you, because Lighthouse loads the page but never interacts with it. You fix INP by first finding the slow interaction in real data, then shortening it.

1. Capture the slow interaction in the field

Search Console tells you that INP is poor, not which interaction is slow. The attribution build of the `web-vitals` library tells you which element was involved and where the time went:

"use client";

import { useEffect } from "react";
import { onINP } from "web-vitals/attribution";

export function InpReporter() {
  useEffect(() => {
    onINP(({ value, attribution }) => {
      window.dataLayer?.push({
        event: "inp",
        inp_value: Math.round(value),
        inp_target: attribution.interactionTarget,
        inp_input_delay: Math.round(attribution.inputDelay),
        inp_processing: Math.round(attribution.processingDuration),
        inp_presentation: Math.round(attribution.presentationDelay),
      });
    });
  }, []);
  return null;
}

Pushing this to the dataLayer lets GTM forward it to GA4 like any other event. After a week you have a list of the elements with the slowest interactions, and each one shows which of the three phases is responsible.

2. Read the three phases

  • Input delay: the main thread was busy with something else when the user tapped. Look at hydration, third-party scripts and timers.
  • Processing duration: your own event handler is slow. This is the most common case and the easiest to fix.
  • Presentation delay: the handler finished, but rendering the result was expensive. Look at large DOM updates and layout thrashing.

3. Show feedback first, do the work after

INP ends at the next paint. You don't have to finish the work, only show a response quickly. Update the visible state first, then yield to the browser before the expensive part:

function yieldToMain() {
  if ("scheduler" in window && "yield" in (window as any).scheduler) {
    return (window as any).scheduler.yield();
  }
  return new Promise((resolve) => setTimeout(resolve, 0));
}

async function onFilterClick(filter: string) {
  setActiveFilter(filter); // cheap: the button updates immediately
  await yieldToMain();     // let the browser paint that
  applyExpensiveFilter(filter);
}

4. In React, mark slow updates as non-urgent

When the expensive part is a React re-render, such as filtering a long list, `useTransition` tells React the update can be interrupted. The input stays responsive while the list catches up:

const [query, setQuery] = useState("");
const [results, setResults] = useState(items);
const [isPending, startTransition] = useTransition();

function onChange(e: React.ChangeEvent<HTMLInputElement>) {
  setQuery(e.target.value); // urgent: the input itself
  startTransition(() => {
    setResults(filterItems(items, e.target.value)); // can wait
  });
}

5. Get third-party scripts off the critical path

High input delay on the first interaction is often caused by chat widgets, heatmaps and tag managers still executing. Load anything that does not need to run immediately with `next/script` and a later strategy:

import Script from "next/script";

<Script src="https://widget.example.com/chat.js" strategy="lazyOnload" />

6. Keep the DOM small where people interact

Presentation delay grows with the number of elements the browser has to recalculate. A table with 2,000 rows re-rendering on every keystroke will fail INP however fast your JavaScript is. Paginate or virtualise long lists, and use `content-visibility: auto` on long off-screen sections so the browser skips rendering them.

The order matters. Measure in the field, find the phase, and fix that phase. Guessing at INP fixes wastes time, because the slow interaction is rarely the one you would have picked.

Related

Got a project with these problems?

Available for remote work worldwide.

Start a conversation →