To fix Interaction to Next Paint (INP) in a Next.js or React app, move expensive work off the main thread, ship less JavaScript, and stop blocking the UI during state updates. The three highest-impact moves are wrapping non-urgent updates in startTransition, pushing rendering into React Server Components so the browser downloads less JS, and deferring or isolating third-party scripts. Together these routinely take an app from a failing INP (>200 ms) into the "good" range.
INP replaced First Input Delay as a Core Web Vital, and it's the one most sites fail — roughly 4 in 10 sites are over the 200 ms threshold. It measures the full latency from a user interaction to the next frame the browser paints, so unlike the old FID, it catches sluggish updates after the click, not just the delay before it. That makes React's render behavior the usual culprit.
What INP measures and why React apps fail it
INP records the worst (or near-worst) interaction latency across a page visit. Three things happen between a tap and the paint, and each is a place React can stall:
- Input delay — the main thread is busy (often hydrating or running a third-party script) and can't react yet.
- Processing time — your event handler and the resulting React render run.
- Presentation delay — the browser lays out and paints the new frame.
A heavy synchronous render in step 2, or a clogged main thread in step 1, is what pushes most React apps over the line.
The 5 most common INP killers in Next.js
In order of how often we see them in audits:
- Hydration delay — large client bundles hydrating on load, blocking early interactions.
"use client"contagion — one client component high in the tree turning whole subtrees into shipped JS.- Synchronous state updates — every interaction triggering a large, blocking re-render.
- Oversized DOM — thousands of nodes making every layout and paint slow.
- Third-party scripts — analytics, chat widgets, and tag managers competing for the main thread.
Fixes that actually move the number
Move non-urgent updates off the critical path
If an interaction kicks off an expensive render (filtering a big list, updating a chart), mark it non-urgent so the browser can paint feedback first:
import { startTransition, useState } from "react";
function Filter({ items }: { items: Item[] }) {
const [query, setQuery] = useState("");
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
// Urgent: keep the input responsive
setQuery(value);
// Non-urgent: let the heavy list update yield to paint
startTransition(() => {
runExpensiveFilter(value);
});
}
return <input value={query} onChange={onChange} />;
}
Ship less JavaScript with Server Components
The cheapest interaction is the one that doesn't need hydration. Keep components as Server Components by default and only add "use client" at the leaves that truly need interactivity. A static product description or article body should ship zero client JS.
Tame third-party scripts
Load non-critical scripts with the right strategy so they stop blocking interactions:
import Script from "next/script";
<Script src="https://example.com/widget.js" strategy="lazyOnload" />;
lazyOnload defers a chat widget or analytics tag until the browser is idle, freeing the main thread during the interactions that count.
Code-split heavy interactions
Don't ship a modal, editor, or charting library until the user needs it:
import dynamic from "next/dynamic";
const RichEditor = dynamic(() => import("./RichEditor"), { ssr: false });
Cause → fix → expected delta
| Cause | Fix | Typical INP impact |
|---|---|---|
| Heavy synchronous render | startTransition / useDeferredValue | High |
| Large client bundle | Server Components, less "use client" | High |
| Blocking third-party scripts | next/script lazyOnload | Medium–High |
| Always-loaded heavy widgets | next/dynamic code splitting | Medium |
| Oversized DOM / long lists | Virtualization, pagination | Medium |
Deltas vary by app, but the first two fixes alone are usually enough to clear the 200 ms bar on a typical Next.js site.
Measure INP the right way
Lab tools approximate; field data is the truth. INP is a field metric, so trust the Chrome User Experience Report (CrUX) and real-user monitoring over a single Lighthouse run. Use the web-vitals library to capture real interactions, and watch the 75th-percentile value — that's what Google grades.
Frequently asked questions
What is a good INP score? 200 ms or less at the 75th percentile is "good." 200–500 ms needs improvement, and over 500 ms is poor.
Why does my Next.js app feel slow even though it loads fast? A fast load (LCP) doesn't guarantee responsiveness. If interactions trigger heavy synchronous renders or the main thread is busy hydrating, INP suffers even when the page appears quickly.
Does switching to Server Components improve INP? Usually yes. Server Components ship less JavaScript, which reduces hydration cost and frees the main thread — directly improving input delay and processing time.
Is INP a ranking factor? INP is a Core Web Vital and part of Google's page experience signals. Beyond ranking, a responsive UI improves conversion regardless of search.
Failing Core Web Vitals? Our web development team does fixed-scope performance audits. Book a call and we'll find what's blocking your main thread.