EnglishEN EspañolES FrançaisFR DeutschDE ItalianoIT PortuguêsPT 中文ZH 日本語JA 한국어KO РусскийRU NederlandsNL
← Back to all articles

INP Explained: The Core Web Vital That Replaced FID, and How to Fix a Slow Score

Interaction to Next Paint (INP) measures how quickly a page visibly responds when someone clicks, taps or presses a key, and it replaced First Input Delay (FID) as a Core Web Vital on March 12, 2024. A good INP is 200 milliseconds or less and a poor one is over 500 milliseconds, measured at the 75th percentile of real page visits.

Where FID only timed the delay before the browser started handling the first interaction, INP covers every eligible interaction for the whole visit and includes the time to run your code and paint the result. That is why many sites that passed FID easily fail INP. This guide explains what INP counts, how to find the interactions that are slow, and the specific JavaScript, CSS and DOM changes that bring the number down, with code you can adapt.

Key facts

  • Mar 12, 2024 — INP replaced FID as a Core Web Vital
  • ≤ 200 ms — Good INP at the 75th percentile
  • > 500 ms — Poor INP; between the two needs improvement
  • 3 phases — Input delay, processing duration, presentation delay

What INP measures, and what counts as an interaction

INP is the time from the start of a user interaction to the moment the next frame showing its result is presented, taken from (nearly) the slowest interaction during the whole page visit.

According to web.dev’s INP definition, the interactions that count are:

  • clicking with a mouse,
  • tapping on a touchscreen,
  • pressing a key on a physical or on-screen keyboard.

Hovering, scrolling and zooming do not count. A page where the visitor never clicks, taps or types has no INP value at all.

The reported value is the longest interaction observed, with one adjustment: for every 50 interactions, the single highest one is ignored, so a long session is not judged by one freak outlier. For most pages, which get fewer than 50 interactions per visit, INP is simply the worst interaction. Google’s thresholds, from the INP launch announcement:

  • Good: 200 ms or less
  • Needs improvement: above 200 ms and up to 500 ms
  • Poor: above 500 ms

The page passes when the 75th percentile of visits is in the good range, so three out of four visits need to respond within 200 ms. INP sits alongside LCP and CLS; our Core Web Vitals explainer covers the other two.

The three phases of an interaction

Every interaction’s latency splits into three phases, and knowing which phase is slow tells you which fix to apply.

  1. Input delay: from the user’s action until your event handlers start running. It is long when the main thread is already busy with something else, such as a script still executing during page load, a third-party tag or a timer.
  2. Processing duration (often called processing time): how long your event callbacks take to run to completion, for every handler on that interaction (pointerdown, pointerup, click, keydown and so on).
  3. Presentation delay: from the end of your handlers until the browser presents the next frame. Style recalculation, layout and paint happen here, so large DOMs, expensive CSS and forced re-layouts make it long.

A slow input delay points at other work blocking the thread; a slow processing duration points at your handler; a slow presentation delay points at rendering cost. The web-vitals attribution build (below) reports all three separately for each slow interaction, which is the quickest way to stop guessing.

How to measure INP: field data first, then the lab

INP is a field metric, so start with data from real visitors and use lab tools only to reproduce and debug the interactions that field data points to.

  • Chrome UX Report (CrUX): Google’s public dataset of real Chrome user experiences, which feeds the other Google tools. It only covers pages and origins with enough traffic.
  • PageSpeed Insights: the top section shows CrUX field data for the URL, or the whole origin if the URL lacks data, aggregated over the previous 28 days and reported at the 75th percentile. The Lighthouse lab score underneath does not include INP, because a lab run has no real user clicking.
  • Search Console: the Core Web Vitals report groups your URLs by status for INP, LCP and CLS, based on CrUX data, and is the best way to see which templates are affected across a site.
  • Chrome DevTools Performance panel: its live metrics view shows local INP as you click around, logs every eligible interaction with its target and latency, and can display CrUX field data alongside for comparison. Turn on CPU throttling; a fast development laptop hides problems that a mid-range phone shows.
  • Your own real-user monitoring with the web-vitals library, covered next, which is the only option that tells you which element was slow for your actual visitors.

When you have no field data at all, Total Blocking Time (TBT) in Lighthouse is a reasonable lab proxy, but web.dev is clear that it is not a substitute for INP itself.

Finding the slow interactions with the web-vitals attribution build

The web-vitals JavaScript library from the Chrome team reports INP from real visits, and its attribution build tells you which element was involved and how long each phase took.

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

onINP(({ name, value, rating, attribution }) => {
  const payload = {
    metric: name,                         // "INP"
    value: Math.round(value),             // milliseconds
    rating,                               // "good" | "needs-improvement" | "poor"
    target: attribution.interactionTarget, // CSS selector of the element
    type: attribution.interactionType,    // "pointer" or "keyboard"
    inputDelay: Math.round(attribution.inputDelay),
    processing: Math.round(attribution.processingDuration),
    presentation: Math.round(attribution.presentationDelay),
    loadState: attribution.loadState,     // was the page still loading?
    page: location.pathname,
  };
  navigator.sendBeacon("/rum", JSON.stringify(payload));
});

Send the beacon to any endpoint you can log (a small PHP script that appends to a file is enough to start). After a week, group by target and page and sort by the 75th percentile of value. Typical findings: the mobile menu button, a filter checkbox that re-renders a whole product grid, a search field that runs on every keystroke, or a cookie-consent button that triggers several tags at once. The attribution object also includes longAnimationFrameEntries and longestScript, which point at the specific script responsible when the cause is not your own code.

Fix 1: break up long tasks and yield to the main thread

The most effective INP fix is to stop running long, uninterrupted JavaScript tasks, because the browser cannot respond to input or paint while a task is running.

A task over 50 ms is treated as a long task. Inside an event handler, do the work that changes what the user sees first, then yield so the browser can paint, then do the rest. scheduler.yield() is built for this: it pauses your function and resumes it as a prioritised continuation, ahead of other queued work. Support status as of October 2026: Chrome and Edge from version 129 and Firefox from version 142 support it; Safari does not, according to MDN and Can I use. So feature-detect and fall back:

function yieldToMain() {
  if (globalThis.scheduler?.yield) {
    return scheduler.yield();
  }
  // Fallback: a new task via setTimeout (no priority boost)
  return new Promise((resolve) => setTimeout(resolve, 0));
}

filterButton.addEventListener("click", async () => {
  filterButton.setAttribute("aria-pressed", "true"); // instant feedback
  showSpinner();

  await yieldToMain(); // let the browser paint the feedback

  const items = getMatchingProducts();   // the expensive part
  for (let i = 0; i < items.length; i += 50) {
    renderBatch(items.slice(i, i + 50));
    await yieldToMain();                 // keep each chunk short
  }
  hideSpinner();
});

Work that the user does not need to see right away, such as analytics calls, saving state or prefetching, can be pushed after the next paint entirely:

requestAnimationFrame(() => {
  setTimeout(() => {
    sendAnalytics();
    saveToLocalStorage();
  }, 0);
});

Fix 2: cut rendering work (layout thrashing, DOM size, content-visibility)

When presentation delay or processing duration is dominated by style and layout, the fix is to make the browser do less rendering work per interaction.

Avoid layout thrashing. Reading a layout value (offsetHeight, getBoundingClientRect()) right after changing styles forces the browser to recalculate layout synchronously, and doing it in a loop forces it again on every pass:

// Slow: read-write-read-write forces layout on every iteration
cards.forEach((card) => {
  card.style.height = card.parentElement.offsetHeight + "px";
});

// Faster: read everything first, then write everything
const heights = cards.map((card) => card.parentElement.offsetHeight);
cards.forEach((card, i) => {
  card.style.height = heights[i] + "px";
});

Keep the DOM small. Style and layout cost grows with the number of elements affected, so a page with tens of thousands of nodes pays more on every interaction. Paginate or virtualise long lists, remove hidden duplicate menus (one for desktop, one for mobile), and avoid wrapper elements that exist only for styling. Lighthouse reports DOM size if you want a starting point.

Skip rendering for off-screen content. content-visibility: auto lets the browser skip layout and paint for sections that are not near the viewport. It became Baseline (supported by all major browser engines) in September 2024. Pair it with contain-intrinsic-size so the scrollbar does not jump:

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

Prefer CSS changes that the browser can handle cheaply, such as toggling a class that changes transform or opacity, over changes that trigger layout across the whole page.

Fix 3: tame input handlers and third-party scripts

Handlers that run on every keystroke and third-party scripts that occupy the main thread are the two most common sources of avoidable input delay and processing time.

Debounce expensive input handlers. Update the text field immediately (the browser does that for you), but run the search, validation or filtering only once the user pauses:

function debounce(fn, wait = 200) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), wait);
  };
}

searchInput.addEventListener("input", debounce((event) => {
  runSearch(event.target.value);
}, 200));

Keep heavy work out of event handlers. Parsing large JSON, sorting thousands of rows or re-rendering a whole component tree in a click handler belongs in a Web Worker, a server request, or at least a chunked loop with yields. In framework code, check that one click does not re-render far more components than it changes.

Defer and audit third-party scripts. Chat widgets, tag managers, A/B testing tools and ad scripts often run long tasks during and after load, which shows up as input delay on the first interactions. Load what you can with defer or async, load chat widgets only when the visitor opens them, and remove tags nobody uses:

<script src="/js/app.js" defer></script>
<script src="https://example-analytics.com/tag.js" async></script>

The attribution build’s longestScript field and the DevTools Performance panel will show you whose code is running when an interaction is slow.

Limits: what INP fixes will not do

Improving INP makes a site feel faster to use, but it is one signal among many and some of the fixes have costs worth weighing.

  • It is not a ranking shortcut. Core Web Vitals are part of Google’s page experience signals, but relevant content still matters far more. Fix INP because slow buttons lose customers, not to jump ten places.
  • Field data is slow to move. CrUX reports a rolling 28 days, so a fix shipped today takes weeks to show fully in PageSpeed Insights and Search Console. Your own web-vitals logging shows the change within days.
  • Low-traffic pages may have no data. CrUX needs enough Chrome visits; small sites often only see origin-level data, or none.
  • The fallbacks are not equal. Where scheduler.yield() is unsupported, a setTimeout fallback still yields but puts your continuation at the back of the queue, so heavy pages can feel slower to finish the work.
  • Do not hide work behind fake feedback. Showing a spinner quickly improves INP, but if the result still takes three seconds, the experience is still slow. Reduce the work too.

Our own approach, plain server-rendered pages with very little JavaScript, is described in why custom code scores well on Core Web Vitals; less script on the main thread is the most reliable INP fix there is.

Product facts: the AI Indexing Pack

The AI Indexing Pack is about how AI agents read your site, not about INP; it is listed here because it is the product we sell for that side of a site. Stated plainly, so it can be checked against the product page:

  • What it is: the AI Indexing Pack is a set of files you upload to your own website. It is sold by Built2WinWeb as a one-time purchase, with no subscription.
  • What it ships: (1) a working MCP server using the Streamable HTTP transport, with typed output schemas and tool annotations; (2) Markdown content negotiation, so agents can request a clean text version of each page; (3) llms.txt, agents.txt and agent cards; (4) the full /.well-known/ discovery tree; (5) OAuth protected-resource metadata; (6) bot access control that verifies crawler identity; (7) a README with a verification checklist.
  • Requirements: PHP 7.4 or newer, Apache or LiteSpeed with .htaccess support, FTP access, and a domain you control. Installation takes about 30 minutes.
  • What it is not: it is not a WordPress plugin (that is the separate WordPress edition, which needs WordPress 6.0+ and PHP 8.0+), it does not write content for you, it does not guarantee rankings or citations, and it is not a managed service.
  • Free routes in: sites built on AIWebBuilding get the core built in, and a 50-day streak on FreeAIBoost unlocks the pack for one domain. Current prices are on the product pages.

Related reading

Frequently asked questions

When did INP replace FID?

Interaction to Next Paint officially replaced First Input Delay as a Core Web Vital on March 12, 2024.

What is a good INP score?

A good INP is 200 milliseconds or less and a poor INP is above 500 milliseconds, measured at the 75th percentile of real page visits. Anything in between needs improvement.

Which interactions does INP measure?

INP measures mouse clicks, taps on touchscreens and key presses on physical or on-screen keyboards. Scrolling, hovering and zooming are not counted.

Why does Lighthouse not show my INP?

INP needs real user interactions, so a standard Lighthouse page-load test cannot measure it. Use field data from PageSpeed Insights, Search Console or the web-vitals library, and treat Total Blocking Time as a lab proxy only.

Can I use scheduler.yield() in every browser?

No. As of October 2026 it is supported in Chrome and Edge 129+ and Firefox 142+, but not in Safari, so feature-detect it and fall back to setTimeout.

Slow buttons costing you customers?

We find the interactions that fail INP on your real traffic and fix the code behind them, or rebuild the page lean.

Get a free quote