# How to fix INP in Next.js and React (2026 playbook)

*By Roberto Lazar, founder of Dock30 · Published 2026-06-10 · Updated 2026-07-25 · 7 min read*

Client-hydrated React and Next.js sites fail INP at about three times the rate of static ones. The causes we see most in audits, and the fixes that work.

To fix Interaction to Next Paint (INP) in a Next.js or React app, do three things: ship less JavaScript by keeping components on the server, move expensive renders off the critical path with `startTransition` or `useDeferredValue`, and load third-party scripts through `next/script` so they stop competing with your users for the main thread. INP is graded on field data at the 75th percentile, and a good score is **200 ms** or less. In our audits, the first two fixes alone are usually enough to get a failing app under that bar.

First, some honesty about the numbers, because the scary version of this article has been written many times. Most of the web passes INP now. In the May 2026 CrUX dataset, **86.6%** of origins had a good INP, and the [Web Almanac 2025](https://almanac.httparchive.org/en/2025/performance) puts good INP at 77% of mobile origins and 97% on desktop. LCP is the vital most sites actually fail, sitting at 68.6% good per the [Core Web Vitals benchmarks for 2026](https://www.digitalapplied.com/blog/core-web-vitals-benchmarks-2026-pass-rate-reference). So why does INP still deserve its own playbook? Because those averages are carried by static and server-rendered sites. JS-heavy, client-hydrated React and Next.js apps fail INP at roughly **three times** the rate of server-rendered or static sites, a gap visible in the [framework benchmarks at webvitals.tools](https://webvitals.tools/benchmarks/). The average brochure site is fine. Your hydrated dashboard probably is not, and neither is an [ecommerce store where slow taps cost conversions](/services/websites-ecommerce).

## What INP measures and why React apps trip it

INP replaced First Input Delay as a Core Web Vital on [March 12, 2024](https://web.dev/blog/inp-cwv-march-12). FID only measured the delay before your event handler started running. INP measures the full latency from a user interaction to the next frame the browser paints, and it reports the worst (or near-worst) interaction across the whole page visit. The grading bands: 200 ms or less at the 75th percentile is good, 200 to 500 ms needs improvement, over 500 ms is poor.

Three things happen between a tap and the resulting paint, and each is a place React can stall:

1. Input delay. The main thread is busy, often hydrating or running a third-party script, so your handler cannot start yet.
2. Processing time. Your event handler runs, and so does the React render it triggers.
3. Presentation delay. The browser recalculates layout and paints the new frame.

FID only saw the first slice. INP sees all three, which is exactly why frameworks that render in the browser felt the change hardest. A heavy synchronous render in step two, or a main thread clogged by hydration in step one, is what pushes most React apps over the line.

## The five INP killers we see in Next.js audits

The same five causes show up in almost every failing app we look at, in roughly this order:

1. Hydration delay. Large client bundles hydrating on load block the first interactions a user attempts, sometimes for seconds on a mid-range phone.
2. `"use client"` contagion. One client component high in the tree turns the whole subtree into shipped, hydrated JavaScript, including the parts that never change.
3. Synchronous state updates. A single keystroke or click triggers a large blocking re-render, and the browser cannot paint anything until React finishes.
4. Oversized DOM. Thousands of nodes make every layout and paint slower, so even a cheap render produces a slow frame.
5. Third-party scripts. Analytics, chat widgets, and tag managers run on the same main thread as your handlers, and they do not queue politely.

## Fixes that actually move the number

### Mark non-urgent updates as transitions

If an interaction kicks off an expensive render (filtering a big list, redrawing a chart), tell React the heavy part is not urgent so the browser can paint feedback first:

```tsx
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} />;
}
```

The input stays responsive because React treats the transition as interruptible work. When the expensive part is derived from a value rather than triggered by an event, `useDeferredValue` does the same job with less ceremony. These two APIs are still the primary interaction-latency levers in React 19.

### Default to Server Components

The cheapest interaction is the one that needs no hydration at all. Keep components on the server by default and add `"use client"` only at the leaves that genuinely handle input. A product description, an article body, a pricing table: none of these should ship client JavaScript. Every kilobyte you keep on the server is main-thread time the browser gets back during the interactions that count. On the [Next.js and NestJS stack we deploy for clients](/blog/fullstack-nextjs-nestjs-postgres-railway), pushing client boundaries down to the leaves has repeatedly cut client bundles by half or more without removing a feature.

### Load third-party scripts on your terms

Next.js 16 gives you three practical loading strategies through [`next/script`](https://nextjs.org/docs/app/api-reference/components/script): `beforeInteractive` for the rare script that truly must run first, `afterInteractive` (the default) for things like tag managers, and `lazyOnload` for everything that can wait until the browser is idle.

```tsx
import Script from "next/script";

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

A chat widget does not need to boot before your buy button responds. In our audits, moving two or three scripts to `lazyOnload` is often worth 50 to 100 ms of input delay on mobile, and it takes ten minutes.

### Code-split heavy widgets

Do not ship a modal, rich-text editor, or charting library until the user asks for it:

```tsx
"use client";

import dynamic from "next/dynamic";

const RichEditor = dynamic(() => import("./RichEditor"), { ssr: false });
```

One App Router gotcha: `ssr: false` only works inside a Client Component, so put the dynamic import in the client file that renders the widget, not in a Server Component. This fix does not make the editor itself faster. It makes every other interaction on the page faster, because the editor's code is no longer parsed, compiled, and hydrated up front.

### Let the React Compiler memoize for you

[Next.js 16](https://nextjs.org/blog/next-16) ships stable React Compiler support, and it is directly relevant here. The compiler auto-memoizes components, so a state change high in the tree no longer re-renders every child that did not actually change. That is a straight cut to processing time, the middle slice of INP, without the `memo` and `useMemo` bookkeeping nobody maintains correctly anyway. Turn it on before you spend a week hand-optimizing renders. Just do not read it as permission to ship more client code; it shortens renders, it does not shrink bundles.

## Which fix targets which cause

The impact column reflects what we see in our own audits, not a published benchmark. Your app will vary.

| Cause | Fix | Impact in our audits |
|---|---|---|
| Heavy synchronous render | `startTransition` / `useDeferredValue` | High |
| Large client bundle | Server Components, fewer `"use client"` boundaries | High |
| Wasteful re-renders | React Compiler (Next.js 16) | Medium to high |
| Blocking third-party scripts | `next/script` with `lazyOnload` | Medium to high |
| Always-loaded heavy widgets | `next/dynamic` code splitting | Medium |
| Oversized DOM, long lists | Virtualization, pagination | Medium |

The first two rows are where we start on nearly every project, and in our experience they are usually sufficient to clear 200 ms.

## Measure in the field, and do not stop at INP

INP is a field metric. A single Lighthouse run on your MacBook tells you almost nothing about the 75th-percentile Android user on a train. Trust CrUX and real-user monitoring (the web-vitals npm package makes capturing real interactions an afternoon of work), and watch the P75 value, because that is what Google grades.

One finding from the [Web Almanac 2025](https://almanac.httparchive.org/en/2025/performance) is worth sitting with: while field INP improved across the web, lab Total Blocking Time went up 58%. Sites are getting better at making the specific interactions users perform feel fast while shipping more background JavaScript than ever. That gap will not stay free forever. A new feature, a slower device market, or one extra third-party tag can convert that latent blocking time into a failing INP in a single release.

And passing INP is not the finish line. Only **48%** of mobile origins pass all three Core Web Vitals (56% on desktop), and LCP fails far more often than INP does. If your CrUX report shows green INP and red LCP, your next sprint is image loading and server response time, not another render optimization.

We have been shipping and fixing production Next.js apps since 2021, for 600+ founders and teams, and performance problems are rarely mysterious once the field data is in front of you. If you would rather have someone else find what is blocking your main thread, our [custom development team](/services/custom-development) does fixed-scope audits with the exact price and delivery date in writing before work starts, from EUR 350 ([how project pricing works](/pricing/project)). Or grab a free 15-minute call at [calendly.com/dock30/15min](https://calendly.com/dock30/15min) and I will tell you honestly whether your INP problem needs an agency or an afternoon.

## Frequently asked questions

**What is a good INP score in 2026?**

A good INP is 200 milliseconds or less at the 75th percentile of real user visits. Scores between 200 and 500 ms need improvement, and anything over 500 ms is poor. Google grades the field data collected in CrUX, not a single lab run, so one fast Lighthouse test does not mean you pass.

**Why do React and Next.js apps fail INP more often than other sites?**

Client-hydrated React apps do their rendering in the browser, so every interaction competes with hydration, re-renders, and third-party scripts for the same main thread. Benchmarks show JS-heavy, client-hydrated sites fail INP at roughly three times the rate of server-rendered or static sites. A static page simply has less JavaScript standing between a tap and the next painted frame.

**Did INP replace First Input Delay?**

Yes. INP replaced FID as a Core Web Vital on March 12, 2024. FID only measured the delay before your event handler started running, while INP measures the full latency from an interaction to the next painted frame, including the React render the interaction triggers.

**Does the React Compiler in Next.js 16 improve INP?**

It helps with one specific cause: expensive re-renders. The compiler auto-memoizes components, so a state change no longer re-renders whole subtrees that did not change, which cuts processing time on interactions. It does not reduce the JavaScript you ship or fix blocking third-party scripts, so treat it as one lever among several.

**If I pass INP, are my Core Web Vitals fine?**

Not necessarily. Only 48% of mobile origins pass all three Core Web Vitals, and LCP is now the most commonly failed metric. Check LCP and CLS in the same CrUX report before you celebrate, because Google evaluates the set, not one number.

---

Written by Roberto Lazar, founder of Dock30. Book a call: https://dock30.com/contact
