When Google replaced First Input Delay with Interaction to Next Paint as an official Core Web Vitals metric, heavy JavaScript frameworks took the hit disproportionately. FID only measured the delay before the browser started processing an input. INP measures the full duration — from the interaction itself to the next frame actually painting on screen.
On large React, Vue, or Angular applications, high INP is almost always caused by long main-thread tasks running during component hydration and state reconciliation, not by any single obviously broken feature.
[User click] → Input delay → Processing time (JS execution) → Presentation delay (render) → [Frame painted]
↑
Main thread blocked hereWhere the time actually goes
Processing time is usually the dominant cost. When a user clicks a filter or menu while hydration or a heavy re-render is mid-flight, that click handler queues behind whatever long task is already running — and tasks over roughly 50ms are exactly what INP is built to penalise.
Practical fixes
Break up large synchronous processing into yieldable chunks so the browser can paint pending updates before your code continues. `scheduler.yield()` is the purpose-built API for this, but it's still an experimental/origin-trial feature in Chromium and isn't available everywhere — always feature-detect it and fall back to a `setTimeout(0)` yield for browsers that don't support it yet, the way the snippet below does:
- In React, wrap non-urgent state updates in useTransition so they're interruptible — if the user interacts again while a transition is calculating, React yields to the new input immediately instead of finishing the stale update first.
- Audit third-party scripts (tag managers, chat widgets, session replay tools) for main-thread cost — running them via requestIdleCallback or offloading to a web worker (Partytown-style) removes a common and avoidable source of hydration-adjacent blocking.
async function handleFilterChange(items) {
setLoadingState(true);
// scheduler.yield() is experimental (Chromium origin trial) — feature-detect
// and fall back to a macrotask yield where it isn't available.
if ('scheduler' in window && 'yield' in window.scheduler) {
await window.scheduler.yield();
} else {
await new Promise((resolve) => setTimeout(resolve, 0));
}
const filtered = performHeavyComputation(items);
setFilteredItems(filtered);
}React's useTransition in practice
import { useTransition } from 'react';
function SearchInput() {
const [isPending, startTransition] = useTransition();
function handleChange(e) {
setInputValue(e.target.value); // high priority: update the input immediately
startTransition(() => {
setSearchQuery(e.target.value); // low priority: defer the heavy filtering
});
}
}Related articles
JavaScript Hydration & SEO: Why 'Rendered HTML' Still Fails Search Engines
"Google renders JS, so it's fine" misses two things: two-wave indexing, and AI crawlers that don't render JS at all. Here's where hydration actually breaks.
React Server Components vs. Traditional SSR: The Technical SEO Comparison
Traditional SSR solves the blank-page problem but still ships a full JS bundle to hydrate. RSC changes that calculation — here's what actually improves.
Dynamic Rendering in 2026: Why Client-Side-Only Rendering Is a Liability for Search
Prerendering for Googlebot used to be a viable patch for client-side apps. It doesn't cover the crawlers that matter most now.

Written by
Paul Lovell
International SEO Consultant & Founder of Always Evolving SEO. 15+ years running technical audits, log-file analysis, and root-cause fixes for multimillion-dollar businesses — speaker at SMX London, Search & Content Summit, and SEMrush events.