The React-shaped INP problem
Interaction to Next Paint measures the delay between a tap and the next frame. React sites fail it in three characteristic ways, and they are different from the ways a WordPress site fails it, so the fixes are different too.
Early taps during hydration. The HTML arrives, the user sees a menu button and taps it, but React is still hydrating the page. The tap's handler is not attached yet, or it is attached but the main thread is busy running hydration for the rest of the tree. The tap waits. On a mid-range phone hydration of a large client tree takes hundreds of milliseconds, and that entire window is INP risk.
Wide re-renders on state change. A keystroke in a search field sets state in a component high in the tree; every descendant re-renders; the frame takes 300 milliseconds. Not slow work, just a lot of components re-rendering that did not need to.
Third-party scripts owning the main thread. Tag managers, chat widgets, session replay, A/B testing. They are the same problem on any stack, but React sites tend to have more of them and to load them earlier, and their long tasks land in the same window as hydration.
Measure before touching anything
INP cannot be reproduced by loading the page. It needs interaction, and the slow interaction is often not the one you would guess. Two measurements, in this order:
Field attribution. The web-vitals library's attribution build, reporting interactionTarget, inputDelay, processingDuration, presentationDelay, loadState and the longest script from Long Animation Frames, sent to your own endpoint. A week of data tells you which element, and which of the three durations is the problem. loadState of dom-interactive or earlier on the slow interactions means hydration; complete means a handler or render cost.
A lab trace of that interaction. DevTools Performance panel, CPU throttled 4x to 6x, record, perform the interaction the field data named, stop. The Interactions track shows the three phases; the main thread flame chart shows what ran. React DevTools' Profiler, recording the same interaction, shows which components rendered and why.
Everything below is chosen by what those two show.
Fix 1: less to hydrate
The most effective fix for hydration-window INP is having less client JavaScript. In the App Router:
- Server Components by default. Anything that does not need state or browser APIs stays on the server and ships no JavaScript. Audit
'use client'directives; most sites have them higher in the tree than needed. - Push
'use client'to the leaves. A page with one interactive widget should have one small client component, not a client page. The server-rendered shell hydrates nothing. - Split heavy islands.
next/dynamicfor components that are below the fold or behind a click, so their code loads later and does not compete with the first interactions. - Check the bundle.
@next/bundle-analyzer. A date library, an icon set imported wholesale, a markdown renderer on the client. Each is hydration time.
Selective hydration (React 18+ with Suspense boundaries) already prioritises hydrating the subtree the user tapped. Suspense boundaries around independent islands make that work; a page with no boundaries hydrates as one unit.
Fix 2: cheaper re-renders
For interactions after load with high processingDuration or presentationDelay:
- State close to where it is used. A search input's value in the search component, not in the page. Moving state down is the single biggest re-render fix.
- React Compiler, which memoises automatically and removes most of the manual
useMemo/memowork. useTransition/startTransitionfor updates that can lag the keystroke: filter the list in a transition so the input's own render paints first.useDeferredValuefor the derived value that drives an expensive render.- Virtualise long lists. A thousand rows re-rendering is a thousand rows re-rendering, memoised or not.
- Avoid layout reads in handlers.
getBoundingClientRectin a loop forces synchronous layout.
Fix 3: third parties off the critical window
next/scriptwithstrategy="lazyOnload"for anything not needed for the first interaction: chat, replay, heat maps, social embeds.- Load on interaction. The chat widget's script fetched when the user taps the chat button, with a lightweight placeholder button rendered immediately.
- Defer the tag manager until after hydration, and configure heavy tags to fire on a timer or scroll, not on load.
- Partytown or a web worker for scripts that can run off-thread, where the trade-off is acceptable.
- Remove what nobody looks at. The heat-mapping tool from a 2023 experiment.
Fix 4: give feedback before the work
INP measures the next paint, not task completion. A handler that immediately sets a "pressed" or "loading" state and paints, then does the heavy work in a transition or after a requestAnimationFrame, scores well even when the work itself is slow. This is not cheating; it is what users experience as responsiveness.
Verifying
Field p75 INP by page, from your attribution endpoint, before and after, over at least a week each. Search Console's report follows over 28 days. In the lab, the same interaction's trace should show the three phases shrinking, and React Profiler should show fewer components rendering per interaction.
Where this sits
INP is the Core Web Vital React sites most often fail after LCP is fixed, and the fix is usually structural (less client tree, state moved down) rather than a tweak. Field attribution first, then the tree, then third parties is the order we work in on Next.js performance engagements, and the first step is the one most teams skip.