How to Fix INP on React and Next.js Sites

How to find and fix slow Interaction to Next Paint on React and Next.js sites: measuring with attribution, shipping less JavaScript, transitions, splitting long tasks and taming third-party scripts.

Interaction to Next Paint (INP) measures how quickly a page responds visually after a user clicks, taps or types. Google’s threshold for a good score is 200 milliseconds or less at the 75th percentile of visits, and above 500 milliseconds is poor. On React and Next.js sites, slow INP usually comes from long JavaScript tasks during hydration, heavy event handlers, expensive re-renders and third-party scripts. The fixes are to ship less JavaScript, split long tasks, mark non-urgent updates as transitions and load third-party code later.

What INP measures

INP replaced First Input Delay as a Core Web Vital in March 2024. For each interaction on a page, it measures the time from the input to the next frame painted on screen, including the delay before the handler runs, the handler itself and rendering. The page’s INP is close to its slowest interaction. A page can load fast and still score badly if a filter, menu or form reacts slowly.

Step 1: find the slow interactions

  • Search Console’s Core Web Vitals report shows which URL groups fail INP, based on real Chrome users.
  • The web-vitals library’s attribution build reports which element and which phase caused each slow interaction from your own users.
  • Chrome DevTools’ Performance panel shows long tasks when you reproduce the interaction.
// app/web-vitals.js: report INP with attribution (client component)
"use client";
import { useEffect } from "react";
import { onINP } from "web-vitals/attribution";

export default function WebVitals() {
  useEffect(() => {
    onINP(({ value, attribution }) => {
      navigator.sendBeacon("/api/vitals", JSON.stringify({
        value,
        target: attribution.interactionTarget,
        type: attribution.interactionType,
        inputDelay: attribution.inputDelay,
        processing: attribution.processingDuration,
        presentation: attribution.presentationDelay,
      }));
    });
  }, []);
  return null;
}

The three phases point to different fixes: a long input delay means the main thread was busy with something else, long processing means your handler is slow, and long presentation delay means rendering the update is expensive.

Step 2: ship less JavaScript

  • Keep pages as server components in Next.js and use “use client” only for the parts that need interactivity. Server components send no JavaScript for themselves.
  • Load heavy components that are not needed immediately with next/dynamic.
  • Check bundle size with the Next.js bundle analyser and remove unused libraries.

Step 3: keep urgent updates fast

React’s useTransition lets the input update immediately while an expensive update, such as filtering a long list, runs as a lower-priority transition:

"use client";
import { useState, useTransition } from "react";

export default function ProductFilter({ products }) {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState(products);
  const [isPending, startTransition] = useTransition();

  function onChange(e) {
    const value = e.target.value;
    setQuery(value);                       // urgent: the input responds at once
    startTransition(() => {                // non-urgent: filtering can wait
      setResults(products.filter((p) => p.name.toLowerCase().includes(value.toLowerCase())));
    });
  }

  return (
    <>
      <input value={query} onChange={onChange} aria-label="Filter products" />
      {isPending && <p>Updating...</p>}
      <ul>{results.map((p) => <li key={p.id}>{p.name}</li>)}</ul>
    </>
  );
}

useDeferredValue achieves the same for values passed down to child components.

Step 4: split long tasks in handlers

If a click triggers several pieces of work, let the browser paint between them. In supporting browsers, scheduler.yield() does this; a setTimeout fallback works elsewhere:

const yieldToMain = () =>
  globalThis.scheduler?.yield ? scheduler.yield() : new Promise((r) => setTimeout(r, 0));

async function onSubmit() {
  showSpinner();          // visible feedback first
  await yieldToMain();
  validateForm();
  await yieldToMain();
  sendAnalytics();        // least urgent last
}

Step 5: reduce rendering cost

  • Avoid re-rendering large trees on every keystroke: move state down to the component that needs it, and use memo where profiling shows it helps.
  • Virtualise long lists so only visible rows render.
  • Avoid reading layout properties such as offsetHeight right after changing styles, which forces extra layout work.

Step 6: control third-party scripts

Chat widgets, tag managers and analytics often cause long tasks. In Next.js, load them with next/script and the afterInteractive or lazyOnload strategy, and remove any that no one uses.

Frequently asked questions

Does INP affect rankings?

Core Web Vitals are part of Google’s page experience signals. Google describes them as one of many factors; relevance and content quality matter more. Good INP still matters for users and conversions.

Why does lab testing show good INP while Search Console shows poor?

Search Console uses real users on real devices, often slower phones. Test on a mid-range Android phone or with CPU throttling in DevTools.

How long until Search Console reflects a fix?

The report uses 28 days of data, so improvements show gradually over about a month.

Sources

  1. web.dev, “Interaction to Next Paint (INP)” and “Optimize INP”
  2. Google Search Central, “Understanding Core Web Vitals and Google search results”
  3. React documentation, useTransition and useDeferredValue
  4. Next.js documentation, next/script and lazy loading