INP in Core Web Vitals: What It Is and How to Improve It

What INP measures, how it differs from FID and which of the three phases to fix first.

  • Good ≤ 200 ms
  • Needs improvement ≤ 500 ms
  • Poor > 500 ms
Arjen Karel
Updated · 12 min read
Share

What is Interaction to Next Paint (INP)?

Interaction to Next Paint (INP) is the Core Web Vital (CWV) for responsiveness: the time from a click, tap or key press to the next frame on screen, measured on the slowest interaction of a page visit. A good INP is 200 milliseconds or less at the 75th percentile.

A visitor feels a bad INP as a button that does nothing for a moment: they tap the menu, nothing changes, so they tap again. INP is one of the three Core Web Vitals, next to LCP and CLS, and Google uses them as a ranking signal.

On a page where people click a lot, INP skips one outlier for every 50 interactions. A visit without a single click, tap or key press has no INP.

INP vs FID: what changed in March 2024?

INP replaced First Input Delay (FID) as a Core Web Vital on March 12, 2024. FID timed only the wait before the browser started to handle the first interaction of a visit. INP times every interaction, from the input to the next frame on screen.

So INP also counts the work FID never saw: the event handlers that run after the wait, and the style, layout and paint before the screen changes. A click whose handler starts after 10 ms and then runs for 300 ms passed FID and fails INP.

First Input Delay (FID)Interaction to Next Paint (INP)
InteractionsThe first oneEvery click, tap and key press
What is timedInput delayInput delay, processing duration and presentation delay
Good100 ms or less200 ms or less
Value per page visitThe delay of the first inputThe slowest interaction

Which interactions does INP measure?

INP measures clicks, taps and key presses, on-screen keyboards included. Every key press while typing in a form field is an interaction of its own. Scrolling, hovering and zooming don't count as interactions.

Scroll code can still make a click slow. A click on an anchor link that starts a JavaScript scroll animation is an interaction, and the scroll code runs inside it. A handler that runs on every scroll event keeps the main thread busy, so a tap that comes in while the visitor scrolls has to wait. scroll-behavior: smooth in CSS gives the smooth scroll without any script.

The three phases of an INP interaction (subparts)

Every interaction that INP measures has three subparts, one after the other: input delay, processing duration and presentation delay. The INP of an interaction is their sum, and the longest subpart is where the fix starts. Chrome DevTools shows all three in the INP breakdown insight of a trace.

Chrome DevTools Performance panel, one tap on a test page: INP 379 ms, INP breakdown with input delay 1 ms, processing duration 369 ms and presentation delay 9 ms

One tap on the "All" filter of a test page built like a holiday-rental listing, recorded in the Performance panel: 369 of the 379 ms is processing duration, most of it layout that the click handler forces again and again. Without a recording, the same tap takes 320 ms: the trace itself slows the tap down.

  • Input delay: the input waits until the task that is already running on the main thread is done. Usually that task is a script that runs during the load: a tag manager, a chat widget or a framework that is still hydrating.
  • Processing duration (also called processing time): the event handlers for the interaction run. A handler is slow when it does too much at once, for example an analytics tag that fires on the click, or a script that reads element sizes after each change and so forces layout again and again.
  • Presentation delay: after the handlers, the browser recalculates styles and layout, then paints the next frame. A large DOM makes this slow, and so does a re-render that changes most of the page. Other tasks that run before that frame count here too.

Core Web Vitals Visualizer: INP 480 ms for a pointerdown while the page was loading, with the INP breakdown input delay 2 ms, processing time 0 ms and presentation delay 478 ms

One tap while the page was still loading, in the Core Web Vitals Visualizer. The handler took 0 ms; the next frame then waited 478 ms for the page's loading work, and a wait after the handler counts as presentation delay.

What is a good INP score?

A good INP is 200 milliseconds or less. Between 200 and 500 milliseconds needs improvement, and over 500 milliseconds is poor. Google takes the 75th percentile (p75) of page loads, for phones and desktop separately.

INP performance bar: GOOD ≤200 ms, NEEDS IMPROVEMENT 200 to 500 ms, POOR >500 ms

At p75, three in four page loads need an INP of 200 ms or less, those on slow phones included. Phones fail first: a slower processor runs the same JavaScript longer, so the main thread is busy more often when the visitor taps.

In CoreDash, INP on phones was about twice that on desktop. The table pools the page loads of 149 projects from October 11 to November 7, 2026:

INP at p75PhonesDesktop
All interactions170 ms89 ms
During the page load285 ms125 ms
After the load158 ms84 ms

In the same data, key presses on desktop were slower than clicks and taps: 121 against 85 ms.

According to the State of Web Vitals, 91.8% of sites have a good INP on phones.

How to measure Interaction to Next Paint (INP)

The INP Google uses comes from real visitors in Chrome: the Chrome UX Report (CrUX), which PageSpeed Insights and Search Console show. A real user monitoring (RUM) tool tells you which click on which page is slow, and Chrome DevTools shows why.

Search Console and PageSpeed Insights

The Core Web Vitals report in Search Console groups the URLs that fail INP by issue, with example URLs for each group. "INP issue: longer than 200ms (mobile)" means that phone visits to that group of similar URLs have an INP over 200 ms at the 75th percentile; "INP issue: longer than 500ms (mobile)" means over 500 ms. Your site can feel fast to you while the report says it fails: many of your visitors use slower phones than you do, and they also tap while the page is still loading. PageSpeed Insights shows the INP at p75 over the last 28 days for one URL, or for the whole site when that URL has too few Chrome visits. A fix shows up slowly in both: Search Console takes up to 28 days.

Google Search Console, Core Web Vitals report on mobile for acme-inp.com: INP issue: longer than 200ms (mobile), need improvement, 1,240 affected URLs, rising over three months

Affected URLs per day for one INP issue in Search Console.

Real user monitoring

CrUX gives one number per URL or group of URLs. A RUM script on your own site measures every visit. In CoreDash I look at INP p75 on mobile first, split by the element that was clicked and by load state.

RUM tools now measure INP in Safari and Firefox too. CrUX, and with it Google, counts only Chrome, so the numbers can differ.

Your own clicks in Chrome DevTools

Open the Performance panel in Chrome DevTools. Its live metrics show the INP of your own session as you click, with a log of every interaction and its three subparts. Set the CPU to 4x slowdown, so your laptop behaves more like a phone, and click while the page is still loading too. To see what ran, record a trace while you click: the Interactions track marks the interaction and the Main track shows the tasks under it. The Core Web Vitals Visualizer extension shows the same breakdown on the page itself.

The web-vitals library logs each interaction that raises the INP to the console, with the element and the subparts:

<script type="module">
  import {onINP} from 'https://unpkg.com/web-vitals@6/dist/web-vitals.attribution.js?module';
  onINP(console.log, {reportAllChanges: true});
</script>

Lighthouse

A normal Lighthouse run has no INP: it loads the page and never clicks. The closest lab number is Total Blocking Time (TBT), the time the main thread was blocked during the load. INP is usually a lot lower than TBT, but improving the TBT will also improve the INP. Why Lighthouse and your field data show different numbers: Lighthouse vs field data.

How to improve INP

The slowest interactions on phones set your INP. Find them in your field data (Search Console or a RUM tool), look at their longest subpart and shorten that one: less JavaScript during the load for input delay, less work in the event handler for processing duration, less to render for presentation delay. The step-by-step INP guide goes from the Search Console report to the line of code.

WordPress

On a WordPress site, start with the plugin scripts each page loads, because each one can make a tap wait. Perfmatters' Script Manager and Asset CleanUp unload a plugin's scripts on the pages that don't use them, such as a form plugin on your product pages. For what is still slow after that, ask a developer for a trace: a recording of the slow tap with a 4x slowed CPU, and the name of the script that runs in it.

The Delay JavaScript settings (Delay JavaScript execution in WP Rocket, Delay JavaScript in Perfmatters, Load JS Deferred set to Delayed in LiteSpeed Cache) hold the scripts back until the first interaction and then run all of them at once. It wins a Lighthouse score, because Lighthouse never touches the page. A real visitor taps the menu and waits while the browser downloads and runs all of it, and INP reports the slowest tap of the visit. Leave those settings off for INP.

WP Core Web Vitals, the plugin from corewebvitals.io, defers the scripts in their original order and runs analytics, pixels and chat widgets after the page has loaded, without waiting for a tap.

Shopify

Every Shopify app adds a bit to every click: you install it for one feature, and its script loads on pages and clicks that have nothing to do with that feature. In the theme editor, under App embeds, switch the apps off one at a time and measure the slow tap again after each one. App blocks you placed in a section stay on when you switch off the app's embed: remove them in that section. An app you removed can leave its code in the theme's snippets, so search the theme for its name.

App settings rarely change what runs in a click. The theme code decides that: what add to cart does, when the cart drawer renders, which app scripts load. That part is work for a developer: the add-to-cart handler updates the screen first and does the rest afterwards, and the cart drawer renders only what changed.

Input delay: less JavaScript during the load

A tap during the load waits for whatever script is running at that moment. The Coverage panel in DevTools shows which code has run so far: a script that is still mostly unused after you have clicked through the page can go, and much of what stays can wait until the page has loaded. Do you really need a chat widget during the first 500 ms of the page load? Start it after the load event, when the browser is idle:

window.addEventListener('load', () => {
  const whenIdle = window.requestIdleCallback || ((cb) => setTimeout(cb, 1));
  whenIdle(() => {
    const script = document.createElement('script');
    script.src = '/js/chat-widget.js';
    document.head.appendChild(script);
  });
});

async and defer keep a script from blocking the HTML parser, but the script still runs during the load, so they help LCP more than INP. Other ways to run a script later are in 16 methods to defer or schedule JavaScript.

The browser waits for a touchstart or wheel listener that isn't passive before it scrolls the page. Adding { passive: true } removes that wait, but INP doesn't measure scrolling, so it doesn't lower your INP.

Split your own long tasks (anything over 50 ms on the main thread), so the browser can handle a waiting tap in between. Give the main thread back (yield) after about 50 ms of work, however long one item takes:

function yieldToMain() {
  if (globalThis.scheduler?.yield) {
    return scheduler.yield();
  }
  return new Promise((resolve) => setTimeout(resolve, 0));
}

async function processItems(items) {
  let lastYield = performance.now();
  for (const item of items) {
    processItem(item);
    if (performance.now() - lastYield > 50) {
      await yieldToMain();
      lastYield = performance.now();
    }
  }
}

scheduler.yield() runs in Chrome and Firefox; Safari gets the setTimeout fallback. More in yield to the main thread.

Processing duration: update the screen first

A fast event handler does what the visitor sees first and leaves everything else until after the next paint. A tag in the click handler runs before the screen can change. Moved after the update, the visitor sees the result first and the tag still runs:

button.addEventListener('click', () => {
  showConfirmation(); // what the visitor sees
  requestAnimationFrame(() => {
    setTimeout(() => {
      // runs after the next frame is painted
      dataLayer.push({ event: 'cta_click' });
    }, 0);
  });
});

A requestIdleCallback() in this place breaks on every iPhone: Safari doesn't have it, so the handler throws an error after showConfirmation() has run, and the click looks fine while the event is lost. The Tag Manager version is in scheduling dataLayer events; on one client site, moving dataLayer.push() after the paint took 100 ms off INP.

For a form or an add to cart that waits for the server, show on screen right away that the tap worked ("Adding to cart...") and let the request finish afterwards.

The slow click I see most often is a product filter: the handler updates the list, reads its new height, changes something based on that height and reads the height again. Each read after a change makes the browser redo the layout inside the handler. On a test page built like a WordPress holiday-rental listing, a tap on the "All" filter took 320 ms, 298 ms of it layout that the theme script forced. With the script reading all heights first, or with CSS doing the script's job, the same tap took 16 ms, a single frame. Both were measured in Chrome at phone width with a 4x slowed CPU. Forced reflow has the fix for themes, plugins, apps and your own code.

Presentation delay: a smaller DOM, fewer re-renders

A big DOM makes every frame after the handlers slower. Lighthouse lists it under "Optimize DOM size": how to make the DOM smaller.

On long pages, content-visibility: auto lets the browser skip the rendering of sections outside the viewport until the visitor scrolls near them:

.product-reviews,
.site-footer {
  content-visibility: auto;
  contain-intrinsic-size: auto 600px;
}

Give every such section a contain-intrinsic-size close to its real height. Without it, a skipped section is zero pixels high, and the scrollbar jumps as the sections render.

Some single page applications re-render far too much content after each interaction: a click changes one counter and half the page renders again. In React DevTools, "Highlight updates when components render" shows which components a click renders.

Debugging INP with the Long Animation Frames (LoAF) API

The Long Animation Frames (LoAF) API reports every frame that took longer than 50 ms, with the scripts that ran in it: the file, the function and how long each one ran. With it, a slow interaction in your field data gets a script name. LoAF works in Chromium browsers (Chrome 123 and later).

new PerformanceObserver((list) => {
  for (const frame of list.getEntries()) {
    if (frame.duration < 100) continue; // only the slowest frames
    for (const script of frame.scripts) {
      console.log(script.sourceURL, script.sourceFunctionName,
        Math.round(script.duration) + ' ms');
    }
  }
}).observe({ type: 'long-animation-frame', buffered: true });

A RUM tool collects these entries from every visit. In CoreDash, open INP, Breakdown and group by LOAF: every row is a script, with the INP of the interactions it ran in, split into the three subparts. Start with the script whose bar is longest on phones. The Long Animation Frames API guide explains each field of an entry.

CoreDash INP subparts by LOAF: p75 INP per script, each split into input delay, processing and presentation.

CoreDash, grouped by LOAF (desktop): green is input delay, purple processing duration, blue presentation delay.

Is the slow tap in a script you didn't write, such as an app, a tag or a consent banner, and you can't tell which one to drop? In a Core Web Vitals power hour we go through your field data and a trace of that tap together, and you get the fix in writing.

About the author

Arjen Karel is a web performance consultant and the creator of CoreDash, a Real User Monitoring platform that tracks Core Web Vitals data across hundreds of sites. He also built the Core Web Vitals Visualizer Chrome extension. He has helped clients achieve passing Core Web Vitals scores on over 925,000 mobile URLs.

Arjen Karel

Need a faster site?

I'm Arjen, and I've been fixing slow sites for 17 years. If you're running into issues you can't fix yourself, send me your URL and let's have a chat.

Send me your URL
Arjen Karel
Arjen Karel, Web performance consultant since 2009

Why isn't our fix working?

Sometimes the CDN still serves the old HTML, or the fix went into the desktop template and mobile uses another one. Send me your URL and the problem, and I reply with a few times. I check your field data first, then we spend one hour on your site, and within a day you have my notes in writing. €350 excl. VAT.

Send me your URL

No developer to build the fix? I do the work myself, on a copy of your site.

Arjen KarelINP still red on mobile?Send me your URL