CORE JSC

International Technology Partnership

Web Development & SEO

Fixing Poor Interaction to Next Paint (INP) Scores Caused by Expensive Event Handlers

LCP and CLS both look fine, but Core Web Vitals field data flags a poor INP score — the page genuinely loaded fast, but clicking a button or opening a dropdown has a noticeable, sluggish delay before anything visibly responds. INP isn't measuring load time at all; it's measuring how long the main thread stays busy after a real user actually tries to interact.

Core JSC Team·September 6, 2026
INPCore Web VitalsPerformanceJavaScriptSEO

The Problem

A site's Largest Contentful Paint and Cumulative Layout Shift both comfortably pass Core Web Vitals thresholds, and initial load feels fast. Yet Search Console or PageSpeed Insights field data flags Interaction to Next Paint as poor — real users experience a noticeable, sometimes very obvious delay between clicking a button, typing into a field, or opening a dropdown, and any visible response appearing. Nothing about the page's load performance explains this, because INP isn't measuring load time at all.

Why It Happens

INP measures the full round trip from interaction to the next painted frame, not just handler execution time

INP captures the entire latency of a single interaction: from the input event, through whatever JavaScript runs in response, to the next frame the browser actually paints. A handler that performs a meaningful amount of synchronous work — a large state update triggering an expensive re-render, JSON.parse/stringify on a large payload, a synchronous loop over a big array — blocks the main thread for that entire duration, delaying the visual response the user is actively waiting for, regardless of how fast the page originally loaded.

Unlike the older FID metric it replaced, INP counts render work after the handler runs, not just delay before it starts

First Input Delay only measured how long a handler had to wait before it could begin executing. INP counts the complete round trip, including whatever rendering work happens as a consequence of the handler — so a handler function that itself returns quickly but triggers an expensive re-render (a large list re-rendering, unmemoized expensive computed values, unrelated subtrees re-rendering unnecessarily) still produces a poor INP score, even though the handler's own execution time looks fine in isolation.

Third-party scripts can delay interaction processing even when first-party code is well optimized

Analytics processing, an ad script, or a chat widget running unrelated work on the main thread at the moment a user interacts can delay when the browser is actually free to process the interaction's queued work — a poor INP score doesn't always trace back to the site's own code, and attributing it correctly requires checking what else was competing for main-thread time during the slow interaction.

INP is measured from real field interactions a lab test may never happen to trigger

Because INP comes from real user interactions in the field rather than a synthetic lab run, a specific slow interaction pattern — one particular dropdown, one specific form field — that a typical dev-environment performance check doesn't happen to exercise can go completely unnoticed until field data surfaces it.

The Fix

1. Profile an actual slow interaction rather than guessing which handler is at fault

import { onINP } from "web-vitals/attribution";

onINP((metric) => {
  console.log(metric.attribution.interactionTarget, metric.attribution.longAnimationFrameEntries);
});

Use Chrome DevTools' Performance panel, or the web-vitals library's attribution build, to record and inspect an actual slow interaction — this identifies exactly what's consuming main-thread time between the input event and the next paint, rather than assuming based on which handler looks suspicious in the source.

2. Break up large synchronous work with yielding so the browser can paint sooner

async function handleClick() {
  updateImmediateVisualState(); // cheap, paints right away
  for (const chunk of chunkedWork) {
    await scheduler.yield?.() ?? new Promise((r) => setTimeout(r, 0));
    processChunk(chunk);
  }
}

Yielding back to the browser between chunks of work — via scheduler.yield() where supported, or a setTimeout-based fallback — gives it the opportunity to paint the interaction's immediate visual feedback before the rest of the work finishes, directly addressing what INP is actually measuring.

3. Memoize expensive computed values and avoid unnecessary re-renders

const filteredItems = useMemo(() => expensiveFilter(items, query), [items, query]);
const MemoizedRow = React.memo(ListRow);

Since INP counts render work triggered by a handler as part of the same interaction latency, memoizing genuinely expensive computed values and preventing unrelated components from re-rendering unnecessarily directly reduces what has to complete before the browser can paint — apply this where profiling actually shows a cost, not blanket everywhere.

4. Audit and defer third-party scripts competing for main-thread time

Check whether analytics, ad, or widget scripts are doing meaningful work on the main thread during typical interaction moments, and defer or lazy-load non-critical third-party code so it's less likely to be competing for the same main-thread time a user's interaction needs processed.

Why This Works

Each fix directly targets what INP actually measures: the time between an interaction and the browser's next paint. Profiling identifies the real bottleneck instead of guessing; yielding gives the browser a chance to paint before all the work finishes; memoization reduces the render cost that counts toward the same interaction latency as the handler itself; and auditing third-party scripts addresses contention for main-thread time that first-party optimization alone can't fix.

Conclusion

A poor INP score alongside otherwise-healthy Core Web Vitals means real users are experiencing genuine delay between interacting and seeing a response — a main-thread problem distinct from load performance, and one FID-era intuitions about "handler execution time" don't fully capture, since INP counts the render work after the handler too. Profile an actual slow interaction rather than guessing, break up large synchronous work with yielding, memoize expensive computations and avoid unnecessary re-renders, and audit third-party scripts for main-thread contention during interaction moments.