Mingqi Hou

Optimizing INP for React Apps (Core Web Vitals)

What Interaction to Next Paint measures, how I profile it, and practical fixes for main-thread blocking, handlers, and rendering.

Interaction to Next Paint (INP) is the Core Web Vital that replaced FID for measuring how snappy a page feels after clicks, taps, and keyboard input. On client sites and SaaS dashboards, poor INP reads as “laggy UI”—and it now factors into search quality signals.

What INP captures

For each interaction, the browser measures delay until the next paint reflects feedback. INP is typically the 95th percentile of those delays across the session (slow outliers matter, not just the first click).

Rough mental model:

INP ≈ interaction delay (JS + event handlers) + time to next paint

Unlike FID, INP considers all interactions on the page, not only the first one.

Measure it

Field data — CrUX, RUM, or web-vitals:

import { onINP } from "web-vitals";

onINP((metric) => {
  analytics.send("inp", { value: metric.value, id: metric.id });
});

Lab — Lighthouse and Chrome Performance panel: record a realistic flow (open modal, submit form, switch tab), inspect long tasks and event timing on the interaction track.

Good targets shift by market; I treat ≤ 200 ms at p75 as healthy for marketing sites and fight anything consistently > 500 ms on primary flows in admin UIs.

Common causes in React apps

  1. Long tasks on the main thread — large sync reducers, parsing huge JSON in click handlers, chart libraries on the critical path.
  2. Heavy render after setState — big lists re-rendered without virtualization.
  3. Layout-thrashing DOM — read/write layout interleaving in loops.
  4. Expensive CSS — animating width/top instead of transform / opacity.
  5. Third-party scripts — analytics or widgets running on every click.
  6. Synchronous work in pointer handlers — doing network + transform in one onClick before showing loading state.

Fixes I apply in production

Break up main-thread work

Make feedback instant

Show perceived progress in the first frame after input:

function SubmitButton() {
  const [pending, setPending] = useState(false);

  async function onClick() {
    setPending(true); // paint spinner before await
    try {
      await submitOrder();
    } finally {
      setPending(false);
    }
  }

  return <button disabled={pending}>{pending ? "Saving…" : "Save"}</button>;
}

Trim render cost

Event listeners

document.addEventListener("scroll", onScroll, { passive: true });

Use delegation carefully; avoid thousands of active listeners on huge DOMs.

Assets and hydration

Workflow on a client project

  1. Identify the top three flows product cares about (checkout, search, save).
  2. Record Performance traces + field INP if available.
  3. Fix longest task first; re-measure.
  4. Add a CI budget or RUM alert on INP regression.

INP is a useful hiring signal: it proves you optimize felt performance, not only Lighthouse scores on a static homepage.