Skip to content
← Tilbage til Learn
Performance & Core Web Vitals Advarsel

Interaction to Next Paint (INP)

INP replaced FID as a Core Web Vital in March 2024. It measures the worst observed interaction delay, not the first. How to find long tasks and fix them.

INP measures the latency between a user interaction (click, tap, keypress) and the next visual update. Unlike FID, which only measured the first input, INP samples the worst observed interaction across the page lifecycle. Google rates it “good” under 200ms, “needs improvement” 200–500ms, and “poor” above 500ms, at the 75th percentile of CrUX field data. INP replaced FID as a Core Web Vital in March 2024 — write INP, not FID.

Why it matters

INP exposes everything FID couldn’t. FID only saw delays before the input handler ran; INP includes the handler, any rendering work it triggers, and the paint that follows. A site that “passed” FID at 30ms can have an INP of 800ms — the JS responded instantly, then ran 500ms of expensive state updates and re-renders.

The bigger product impact: high INP feels broken. Users tap, nothing happens, they tap again, the page jumps. Bounce, frustration, and engagement signals follow. The ranking effect of INP itself is small (page experience is a tiebreaker); the engagement collateral is much larger.

How to detect it

CrUX field data via PageSpeed Insights gives the 75th-percentile INP for any URL with enough traffic. For deeper debugging, the Web Vitals JS library reports per-interaction:

<script type="module">
  import { onINP } from 'https://unpkg.com/web-vitals?module';
  onINP(({ value, entries }) => {
    const longest = entries[0];
    console.log('INP:', value, 'Target:', longest?.target, 'Type:', longest?.name);
  }, { reportAllChanges: true });
</script>

entries[0].target points to the actual element. Combined with Chrome DevTools → Performance → Interactions track, you can see exactly which task on the main thread blocked the paint.

The fix

INP is dominated by main-thread work. Three categories of fix:

Fix 1 — Break up long tasks

A “long task” is anything that blocks the main thread for more than 50ms. Yielding to the browser between chunks lets pending input events render:

// Bad: one 400ms task
function processAll(items) {
  for (const item of items) heavyWork(item);
  updateUI();
}

// Good: yield between batches
async function processAll(items) {
  for (let i = 0; i < items.length; i++) {
    heavyWork(items[i]);
    if (i % 50 === 0) await new Promise(r => setTimeout(r, 0));
  }
  updateUI();
}

For a more rigorous approach, the Scheduler API:

if (typeof scheduler !== 'undefined' && scheduler.yield) {
  for (const item of items) {
    heavyWork(item);
    await scheduler.yield();
  }
}

Fix 2 — Defer non-urgent state updates

In React, mark non-urgent updates as transitions so they don’t block input rendering:

import { useTransition, useState } from "react";

function SearchBox() {
  const [query, setQuery] = useState("");
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    setQuery(e.target.value);  // urgent: input must update
    startTransition(() => {
      runExpensiveFilter(e.target.value);  // non-urgent: can be interrupted
    });
  }

  return <input value={query} onChange={handleChange} />;
}

Vue has defineAsyncComponent and Suspense for similar effects. Svelte’s reactivity is finer-grained but the same principle applies: don’t run expensive work synchronously inside an event handler.

Fix 3 — Reduce JavaScript payload

Less code = less parse, less compile, less execution. The fastest INP win for most sites is removing third-party scripts:

  • Audit <script> tags. Tag managers, A/B testing tools, session replay, chat widgets all add main-thread work.
  • Load non-critical scripts with defer or after requestIdleCallback.
  • Lazy-load components that aren’t visible on first paint.
<script src="/analytics.js" defer></script>

Fix 4 — Use content-visibility for off-screen content

Tells the browser to skip rendering work for elements outside the viewport until they’re scrolled into view:

.below-fold-section {
  content-visibility: auto;
  contain-intrinsic-size: 800px;  /* approximate height */
}

Next.js / React

// Defer non-urgent updates
const [isPending, startTransition] = useTransition();

// Memoize expensive children
const MemoizedList = memo(ExpensiveList);

// Split bundles
const HeavyComponent = dynamic(() => import("./HeavyComponent"));

WordPress

Common culprits: page builders (Elementor, Divi) that emit large runtime bundles, third-party plugins that bind to every input event. Audit with the Query Monitor plugin → JavaScript tab.

Common pitfalls

  • Writing FID in 2026. FID was deprecated in March 2024. The metric is INP.
  • Optimizing the average instead of the 75th percentile. Google measures p75. A median INP of 100ms with a p75 of 800ms is failing.
  • Re-rendering the whole app on every input. React without memoization, Vue without computed properties — every keystroke triggers a full re-render. Profile to confirm.
  • Synchronous localStorage reads in handlers. Disk IO on the main thread. Move to memory cache.
  • Long-running event handlers attached to scroll, mousemove, or input. These fire dozens of times per second. Debounce or throttle.
Fra diagnose til live rettelse

Find issue'et. Få rettelsen live.

Brug Learn til at forstå problemet, og kør derefter Serpwise på dit eget site for at se, hvad der kan rettes og komme live.