Paul LovellBook a call
Back to Blog
7 min readPaul Lovell

Optimising INP for Heavy JavaScript Frameworks

INP measures total interaction time, not just when the browser started responding. On heavy JS frameworks, hydration is usually the hidden cost.

Core Web VitalsJavaScript SEOTechnical SEO

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 here

Where 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
    });
  }
}
Paul Lovell

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.