/ โ† Core Web Vitals Fixes / Poor INP
In this guide
  1. What INP actually measures
  2. The usual causes, by phase
  3. How to find the interaction that fails
  4. Code patterns that fix each phase
  5. The fix, step by step
  6. Platform notes
  7. Edge cases and misleading readings
  8. Keeping INP good
  9. Questions and answers

What INP actually measures

Every time a visitor clicks, taps or presses a key, the browser has to run whatever JavaScript is listening for that input and then draw a new frame showing the result. INP is the time from the input to that next frame. Hovering and scrolling are not counted; only discrete interactions are.

The browser records every qualifying interaction during the whole visit, not just the first. When the visitor leaves, the page's INP is the slowest of those interactions. On pages where people interact a great deal, one outlier is ignored for every 50 interactions, so a single freak delay on a long session does not dominate. Google then takes the 75th percentile of those per-visit values across all real Chrome users over 28 days.

INP at the 75th percentileRatingWhat the visitor feels
200 ms or lessGoodThe page reacts almost at once.
Over 200 ms up to 500 msNeeds improvementA noticeable pause; people start tapping twice.
Over 500 msPoorThe page feels frozen or broken.

The three phases of one interaction

Each interaction breaks into three parts, and the fix depends entirely on which one is large:

  1. Input delay. The wait before your event handler even starts, because the main thread was busy with something else: a script still loading, a timer firing, an analytics tag doing its work.
  2. Processing duration. The time your own handlers for that input take to run, including every listener attached to the element and its ancestors.
  3. Presentation delay. The time after the handlers finish until the frame is painted: style recalculation, layout, paint and compositing of whatever changed.

A menu that opens slowly because a chat widget was busy has an input-delay problem. A filter that re-sorts 2,000 products synchronously has a processing problem. A button that toggles a class on the <body> and forces the browser to restyle a DOM of 6,000 nodes has a presentation problem. Treating all three the same way wastes time.

๐Ÿ’ก INP replaced First Input Delay as a Core Web Vital in March 2024. FID only looked at the input delay of the first interaction. A page with a good FID can still have poor INP, because INP also counts handler time, rendering time and every later interaction.

The usual causes, by phase

PhaseTypical causeWhere it comes from
Input delayLong tasks already running when the visitor tapsLarge bundles still executing after load, hydration in JavaScript frameworks, tag managers firing many tags, consent platforms, session-recording scripts, setInterval timers
Input delaySeveral listeners queued on the same eventAnalytics click tracking, heatmap tools and A/B testing scripts all listening to every click
ProcessingHeavy synchronous work in the handlerFiltering or sorting large arrays, building big chunks of HTML as strings, JSON parsing, synchronous localStorage access in loops
ProcessingFramework re-rendering too muchA state change high in a component tree that re-renders the whole page instead of the one widget that changed
ProcessingForced synchronous layoutReading offsetHeight or getBoundingClientRect() straight after writing styles, often inside a loop
PresentationVery large DOMMega-menus rendered in full on every page, long product grids, comment threads, hidden tab content that is still in the DOM
PresentationExpensive style changesToggling a class high in the tree that invalidates styles across the page, heavy box-shadow or filter effects repainted on every frame

Third-party scripts turn up in more than one row. They add long tasks that cause input delay, and some attach their own click listeners, which adds to processing time for every tap on the page.

How to find the interaction that fails

INP is a field metric. Lighthouse cannot measure it because nobody clicks anything during a lab run; it shows Total Blocking Time instead, which hints at input delay during load but says nothing about a slow handler on a filter button. You need data from real visitors first, then a lab session to reproduce what they did.

Start from the field

The Core Web Vitals checker shows the field INP for the URL and the origin from the Chrome UX Report, on mobile and desktop. The Search Console Core Web Vitals report groups URLs with the same problem, which usually points to one template. Neither tells you which button was slow, so the next step is to collect that yourself.

Collect attribution from your own visitors

Google's open-source web-vitals library has an attribution build that reports, for the interaction that set INP, the element that was interacted with, the type of input and the length of each phase. Send it to your analytics and you will see which elements fail most often.

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

onINP(({ value, rating, attribution }) => {
  const a = attribution;
  sendToAnalytics({
    metric: 'INP',
    value: Math.round(value),
    rating,                        // 'good' | 'needs-improvement' | 'poor'
    target: a.interactionTarget,   // a CSS selector for the element
    type: a.interactionType,       // 'pointer' or 'keyboard'
    inputDelay: Math.round(a.inputDelay),
    processing: Math.round(a.processingDuration),
    presentation: Math.round(a.presentationDelay),
    loadState: a.loadState         // was the page still loading?
  });
});

A few days of data is usually enough. Sort by target and look for the selectors that appear again and again with a poor rating. A loadState of loading or dom-interactive means people tapped before the page settled, which points at input delay from start-up scripts rather than at the handler.

Reproduce it in DevTools

Open Chrome DevTools, go to the Performance panel and set CPU throttling to a 4x or 6x slowdown so a desktop behaves more like a mid-range phone. Recent versions of Chrome show live interaction timings in this panel as you click, which is a quick way to spot the slow control. For detail, start a recording, perform the interaction, stop the recording and find the interaction in the Interactions track. Hovering over it shows the input delay, processing duration and presentation delay. Tasks with a red corner in the Main track are long tasks over 50 milliseconds; expand them to see which script and which function ran.

โš ๏ธ Always test logged out, with a cold cache and the cookie banner still showing, as a first-time visitor sees it. Many INP problems only happen while consent tools and tags are still starting up.

Code patterns that fix each phase

Yield to the main thread inside long work

The single most useful technique is to split one long task into several short ones so the browser can paint and handle other input in between. scheduler.yield() does this in Chromium-based browsers; older browsers can fall back to a zero-length timeout.

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

async function applyFilters(products, filters) {
  const results = [];
  for (let i = 0; i < products.length; i++) {
    if (matches(products[i], filters)) results.push(products[i]);
    if (i % 200 === 0) await yieldToMain();   // let the browser breathe
  }
  return results;
}

Show feedback first, do the rest after the paint

The visitor needs to see that their tap worked. Update the button, spinner or counter straight away, then push anything that is not needed for that frame, such as analytics calls, saving to storage or recalculating recommendations, until after the browser has painted.

button.addEventListener('click', () => {
  button.classList.add('is-loading');   // visible change, cheap
  requestAnimationFrame(() => {
    setTimeout(() => {                 // runs after the next paint
      updateBasket();
      trackAddToBasket();
    }, 0);
  });
});

Avoid forced layout in loops

Reading a layout property after writing a style forces the browser to calculate layout immediately. Inside a loop that becomes dozens of layouts in one handler. Read everything first, then write everything.

// Slow: read, write, read, write ...
cards.forEach(c => { c.style.height = c.offsetWidth * 0.75 + 'px'; });

// Faster: all reads, then all writes
const widths = cards.map(c => c.offsetWidth);
cards.forEach((c, i) => { c.style.height = widths[i] * 0.75 + 'px'; });

Keep off-screen rendering cheap

For long pages with many sections, content-visibility: auto lets the browser skip rendering work for sections far outside the viewport. Pair it with contain-intrinsic-size so the scrollbar does not jump. For lists of hundreds or thousands of rows, render only the visible rows (virtualisation) instead of all of them.

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

The fix, step by step

Step 1

Find the slow interactions in field data

Run the page through the Core Web Vitals checker and note the field INP on mobile and desktop, then open the Search Console Core Web Vitals report to see which URL groups share the problem. Add the web-vitals attribution build to the affected template and send the interaction target and the three phase timings to your analytics. After a few days, list the five selectors that most often produce a poor rating. Those are your targets.

Step 2

Reproduce the interaction with a throttled CPU

Open the page in Chrome as a first-time visitor, with the cookie banner still showing and a cold cache. In the DevTools Performance panel, set a 4x or 6x CPU slowdown, start recording, perform the interaction that your field data named, then stop. Find it in the Interactions track and confirm its duration is over 200 milliseconds. If it is fast even with throttling, test during page load, when start-up scripts are still running.

Step 3

Split the delay into its three phases

Hover over the interaction in the recording and read the input delay, processing duration and presentation delay, or compare them with the phase timings your analytics collected. Whichever phase is largest decides the fix. A large input delay means something else was hogging the main thread. A large processing duration means your handlers are too heavy. A large presentation delay means the style, layout and paint work after the handler is too expensive for the page.

Step 4

Cut input delay by clearing the main thread

Expand the long tasks just before the interaction and note which scripts own them. Load non-essential third-party tags after consent or after the page has finished loading, remove tags nobody uses, and move chat widgets to load on first click of a launcher button. For your own start-up code, split large bundles, defer anything not needed for the first view, and break long initialisation loops with scheduler.yield() so a tap can be handled between chunks.

Step 5

Shorten the event handler itself

Make the handler do only what the next frame needs: update the button state or show a spinner, then defer analytics, storage writes and background recalculation until after the paint. Split loops over large data sets into chunks that yield, move pure computation such as sorting or parsing into a Web Worker, and remove forced layout by reading all measurements before writing styles. In React, wrap non-urgent state updates in startTransition so typing and clicking stay responsive.

Step 6

Reduce the rendering work after the handler

If presentation delay is the large phase, shrink what the browser has to restyle and redraw. Change classes on the smallest element that needs them rather than on body, keep the DOM smaller by rendering mega-menu panels and hidden tabs only when opened, virtualise long lists, and add content-visibility: auto to long below-the-fold sections. Replace animated box-shadow, filter or layout properties with transform and opacity, which the compositor handles cheaply.

Step 7

Verify with real-user data

Repeat the throttled recording and confirm the interaction now finishes well under 200 milliseconds, with no single long task inside it. Deploy, then watch the phase timings from your web-vitals reporting; they will show the change within days. The Chrome UX Report and Search Console use a rolling 28-day window, so expect the field INP in the Core Web Vitals checker to improve gradually. Start validation in Search Console once the fix is live on every page in the group.

Platform notes

WordPress

Poor INP on WordPress sites nearly always comes from plugins that add JavaScript to every page: sliders, pop-up builders, chat, social feeds and several analytics tools at once. Page builders also produce very deep DOMs, which feeds presentation delay. Check which scripts own the long tasks, dequeue plugin scripts on pages that do not use them, and prefer the "delay JavaScript until interaction" setting in a performance plugin only for tags that genuinely are not needed, since delayed scripts all run at once on the first tap and can make that first interaction worse. The WordPress Core Web Vitals guide covers this in more depth.

Shopify

Installed apps inject scripts into the storefront, and each can listen for clicks on product forms and cart buttons. Variant pickers and cart drawers that re-render a large part of the page on every change are common processing-time problems. Remove apps you no longer use, since some leave code behind in the theme, and test the add-to-basket and variant selection interactions specifically.

React, Next.js and other frameworks

Hydration makes the page unresponsive until the framework has attached its handlers, which shows up as input delay for early taps. Keeping more components on the server, splitting client bundles and lazy-loading interactive widgets that sit below the fold all help. Inside components, use startTransition or useDeferredValue for expensive updates such as filtering, and memoise large lists so a small state change does not re-render them. The Next.js and Nuxt guide goes further into server and client components.

Hand-written and static sites

Here the usual culprits are third-party tags and older libraries such as jQuery plugins that bind many handlers and do heavy DOM work on every event. Event delegation, a single listener on a parent rather than one per item, reduces set-up work and memory, though it does not make an individual handler faster on its own.

Edge cases and misleading readings

โš ๏ธ Do not "fix" INP by stopping a button from giving feedback until work is done in the background with no visible change. INP measures the next paint, but a visitor who sees nothing happen will tap again. Always paint a visible response first.

Keeping INP good

Questions and answers

1. What is a good INP score?

Google rates Interaction to Next Paint as good when it is 200 milliseconds or less, needs improvement between 200 and 500 milliseconds, and poor above 500 milliseconds. The rating is based on the 75th percentile of real visits over 28 days, measured separately for mobile and desktop. That means at least three out of four visits must have their slowest interaction at or under 200 milliseconds for the page to pass. Lab tools cannot give you this figure directly.

2. Why does Lighthouse not show INP?

Lighthouse loads the page once on a simulated device and never clicks, taps or types, so there are no interactions for it to measure. It reports Total Blocking Time instead, which counts how long the main thread was blocked during loading. High TBT often goes with poor INP because the same scripts cause both, but a page can have low TBT and still have a slow handler on a filter or menu button. You need real-user data or a manual DevTools recording to measure INP.

3. Which interactions count towards INP?

Clicks with a mouse, taps on a touchscreen and key presses on a physical or on-screen keyboard all count. Hovering, scrolling and zooming do not. The browser watches every qualifying interaction for the whole time the visitor is on the page, not only during loading. For most pages the reported value is the slowest interaction of the visit, while on pages with very many interactions one outlier is ignored for every 50 so that a single freak delay does not dominate.

4. Is poor INP always caused by my own JavaScript?

No. Third-party scripts are a very common cause. Tag managers, analytics, advertising, consent platforms, session recording and chat widgets all run on the same main thread as your code. Their long tasks delay the start of your handlers, and some add their own click listeners that run on every tap. Record the interaction in DevTools and expand the long tasks to see which script owns them before assuming the problem is in your own code.

5. What does scheduler.yield() do and can I use it everywhere?

It pauses your function and lets the browser handle other work, such as painting or responding to input, before continuing where it left off. It is used to split one long task into several short ones. It is available in Chromium-based browsers such as Chrome and Edge, and support elsewhere varies, so wrap it in a small helper that falls back to a zero-length setTimeout when it is missing. The fallback is slightly less efficient but still breaks up the task.

6. Will a faster server improve INP?

Usually not by much. INP happens after the page has loaded, when a visitor interacts, and it depends on how busy the browser's main thread is and how much work your handlers and rendering do. Server speed affects Time to First Byte and Largest Contentful Paint. The exception is an interaction that waits for a network request before showing anything; showing immediate visual feedback, such as a spinner, keeps INP low even when the request itself is slow.

7. How long until Search Console shows my INP fix?

Search Console and the Chrome UX Report use a rolling 28-day window of real Chrome visits, so an improvement appears gradually as old visits drop out, and it can take close to four weeks to be fully reflected. Your own reporting with the web-vitals library will show the change within a few days. Once the fix is live across every page in the affected group, start validation in the Core Web Vitals report and let it run to completion.

Check your INP in real-user data

The Core Web Vitals checker shows real-user field data and a Lighthouse lab test side by side for your URL, breaks down each metric and points to the assets and code behind every failure, so you can see whether INP is the metric holding the page back.

Open Core Web Vitals Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All Core Web Vitals Fixes  ·  High Total Blocking Time  ·  Long Tasks  ·  Third-Party Scripts  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.