# How to fix Interaction to Next Paint (INP) and slow taps

> INP measures how long your page takes to react to a tap or click. Learn what makes it slow, how to find the interaction at fault, and fix it by breaking up long tasks and trimming scripts.

Updated 2026-09-25 · Speed & Core Web Vitals · HTML version: https://getreport.app/guides/fix-interaction-to-next-paint

Interaction to Next Paint (INP) is how long a page takes to show a reaction after someone taps, clicks or types. A menu that opens 600 ms after the tap, a filter that freezes the product grid, a checkout button that seems dead for a second: that is a poor INP. It became a Core Web Vital in March 2024, replacing First Input Delay, and it is the vital most sites that passed before now fail. This guide explains where the time goes, how to find the slow interaction, and the fixes, most of which come down to running less JavaScript at the wrong moment.

## Quick answer

| INP | Rating |
| --- | --- |
| ≤ 200 ms | Good |
| 200–500 ms | Needs improvement |
| > 500 ms | Poor |

- INP only exists in **field data** from real Chrome users; the [Core Web Vitals checker](https://getreport.app/tools/core-web-vitals) shows it when Google has enough visits. Lab tools report **Total Blocking Time** as the stand-in; a high TBT usually means a high INP.
- The cause is almost always **JavaScript on the main thread**: long tasks, heavy click handlers, third-party tags.
- **Break up long tasks** and **yield** to the browser (`scheduler.yield()` or `setTimeout`) so it can paint between chunks.
- In event handlers, **show the visual response first**, then do the heavy work.
- **Remove or delay third-party scripts** nobody uses, and **defer** everything not needed for the first screen.
- Keep the **DOM small**; every update has to lay it out again.

## Why INP matters

A page that ignores a tap feels broken, even if it loaded quickly. Visitors tap again, and the second tap lands on whatever appeared in the meantime. On a shop the slow interactions are the valuable ones: add to cart, size selection, filters, the checkout form. Each one that hangs for half a second is a chance to give up.

INP is one of the three Core Web Vitals Google uses in its page experience signal, judged at the 75th percentile of real Chrome visits over 28 days. Its predecessor, First Input Delay, only measured the wait before the first interaction's handler started; INP covers every click, tap and key press during the visit, and the whole time until the screen updates. Hovering and scrolling are not counted. The overview of all three vitals is in [Core Web Vitals for site owners](https://getreport.app/guides/core-web-vitals-for-site-owners).

## The three phases of an interaction

Every interaction has three parts, and INP is their sum for the slowest interaction of the visit (on pages with many interactions, a few outliers are ignored):

1. **Input delay**: the tap arrives, but the browser is busy with something else (a script parsing, a tag firing, a timer). The handler waits.
2. **Processing duration**: your event handlers run. Everything they do, including code added by analytics or tag managers listening to the same click, counts.
3. **Presentation delay**: the browser recalculates styles, lays out the page and paints the new frame. A huge DOM or a handler that changed a lot of layout makes this long.

The browser can only do one of these things at a time on the main thread. Anything that holds the main thread for a long stretch, a **long task** of more than 50 ms, delays whatever interaction lands during it.

## Why lab tests show TBT instead of INP

A lab tool like Lighthouse loads the page once and never taps anything, so there is no interaction to measure. It measures **Total Blocking Time** instead: the total time, during the load, that long tasks blocked the main thread beyond 50 ms each. A page with 1,200 ms of TBT would have ignored a tap for most of its load.

TBT and INP usually move together, but not always. TBT only covers the load; INP covers the whole visit, including the interactions that run a lot of code (opening the mega menu, filtering 200 products, typing in a search box with live suggestions). A page can have a low TBT and still a poor INP if one specific interaction is slow.

The real INP comes from the Chrome UX Report (CrUX), which needs enough real Chrome visits. Small sites and new pages often have none; then the lab findings are the best guide, and the fixes are the same.

## How getReport checks it

> **Free tool:** [Core Web Vitals checker](https://getreport.app/tools/core-web-vitals): LCP, INP and CLS for your page from real Chrome users, plus the lab values and the exact element or script responsible. Pass or fail against Google’s thresholds.

The Core Web Vitals checker shows the three vitals for the page from real Chrome users next to a Lighthouse lab run. INP comes only from the field data; when Google has no data for the page or the origin, the finding says "not measured yet" as information and costs no points.

![Core Web Vitals result panel: the module score ring, the lab table with LCP failing at 10.5 s and CLS at 0.271, the field column saying there is no CrUX data, and the INP row shown as not measured yet](https://getreport.app/guides/img/fix-interaction-to-next-paint/vitals.webp "INP needs real users: until Google has enough Chrome visitors for the page, the row is information, not a fail, and Total Blocking Time is the lab stand-in.")

> **Check: Interaction to Next Paint.** INP measures how quickly the page reacts when someone taps or clicks. Lab tools cannot simulate it, so this comes from real Chrome users; slow responses feel like a frozen page.
>
> 1. Break up long JavaScript tasks and defer third-party scripts so the main thread is free when people interact.
> 2. Respond to input visually first (a pressed state, a spinner), then do the heavy work.
> 3. Reduce DOM size and avoid layout-heavy work inside click handlers.

The [speed test](https://getreport.app/tools/speed-test) and the full report add the lab findings that explain a poor INP. Start with TBT, then the long tasks list, which names the scripts behind the longest tasks:

> **Check: Total Blocking Time.** TBT adds up every moment the page was too busy running JavaScript to respond to a tap. It is the lab stand-in for INP and the heaviest-weighted Lighthouse metric.
>
> 1. Remove or defer JavaScript that is not needed for the first screen (defer, async, or load on interaction).
> 2. Split long tasks (> 50 ms) into smaller chunks; move heavy work to a web worker.
> 3. Audit third-party tags — chat widgets, tag managers and A/B tools are the usual culprits.

> **Check: No long main-thread tasks block interaction.** A task longer than 50 ms freezes the page for that long. Several in a row are what makes a site feel unresponsive to taps and scrolls.
>
> 1. Find the scripts behind the longest tasks (listed below) and defer, split or remove them.
> 2. Yield to the browser between chunks of work (setTimeout, scheduler.yield, requestIdleCallback).

> **Check: Main-thread work.** This is the total time the browser spent parsing, compiling and running your page's code. While it works, it cannot respond to the visitor.
>
> 1. Reduce JavaScript first — it dominates this number on almost every site.
> 2. Cut style and layout work by simplifying CSS selectors and DOM size.

> **Check: Third-party code weight.** Tags, widgets and embeds from other companies run on your visitors' phones at your page's expense. Over 250 KB or 250 ms of blocking is a sign they dominate the load.
>
> 1. List what each third party gives you; remove the ones nobody looks at.
> 2. Load the rest after interaction or with a facade (a static thumbnail for YouTube, a click-to-load chat button).
> 3. Move tags into a tag manager that fires them late, not in <head>.

The third-party finding groups scripts by company (tag manager, chat, analytics, ads) with their size and blocking time. It is often the fastest route to a better INP, because removing a tag is easier than rewriting your own code.

## Step by step

### 1. Find the slow interaction

Field data tells you INP is poor, not which interaction is slow. Two ways to find it:

**In your browser.** Open Chrome DevTools, go to the Performance panel, and set CPU throttling to 4× slowdown to behave like a mid-range phone. The live metrics view shows your own INP as you click around the page, with a log of each interaction and its time. Try the things visitors do: open the menu, pick a size, add to cart, filter, type in search. When one is slow, record it with the Performance panel and look for the long task under the interaction.

**From real visitors.** Google's `web-vitals` library reports INP with attribution: which element was interacted with and how long each phase took. Add it to your main JavaScript bundle and send the result to your own analytics endpoint:

```js
// main.js (bundled): report INP with attribution
// npm install web-vitals
import { onINP } from 'web-vitals/attribution';

onINP(({ value, rating, attribution }) => {
  const body = JSON.stringify({
    inp: Math.round(value),                  // ms
    rating,                                  // 'good' | 'needs-improvement' | 'poor'
    target: attribution.interactionTarget,   // CSS selector of the element
    inputDelay: Math.round(attribution.inputDelay),
    processing: Math.round(attribution.processingDuration),
    presentation: Math.round(attribution.presentationDelay),
  });
  navigator.sendBeacon('/api/vitals', body);   // your own endpoint
});
```

After a few days the slowest targets and the dominant phase are obvious. A long input delay points at something else running (tags, timers); long processing points at the handler; long presentation points at DOM size and layout.

### 2. Show the response first, then do the work

The next paint is what INP waits for. If the handler updates the UI and then runs 300 ms of calculation, the visitor sees nothing for 300 ms. Reverse it: make the visual change, give the browser a chance to paint, then continue.

```js
// A helper that hands control back to the browser
function yieldToMain() {
  if (globalThis.scheduler?.yield) {
    return scheduler.yield();
  }
  return new Promise((resolve) => setTimeout(resolve, 0));
}

addToCartButton.addEventListener('click', async () => {
  addToCartButton.classList.add('is-loading');   // visible at once
  addToCartButton.disabled = true;
  await yieldToMain();                           // the browser paints here
  await updateCart();                            // the heavy work
  addToCartButton.classList.remove('is-loading');
  addToCartButton.disabled = false;
});
```

`scheduler.yield()` is available in Chromium-based browsers; the `setTimeout` fallback works everywhere. The difference is that `scheduler.yield()` resumes your code before other queued tasks, so the work finishes sooner.

### 3. Break up long tasks

A loop that processes 500 products, renders a long list or parses a big JSON response in one go is one long task. Split it and yield between chunks:

```js
// Process items in chunks of about 50 ms, yielding in between
async function processInChunks(items, handle) {
  let lastYield = performance.now();
  for (const item of items) {
    handle(item);
    if (performance.now() - lastYield > 50) {
      await yieldToMain();
      lastYield = performance.now();
    }
  }
}
```

Work that has nothing to do with the screen (sorting data, heavy calculations) can move to a Web Worker, which runs off the main thread entirely.

### 4. Avoid layout thrashing

Reading a layout value (`offsetHeight`, `getBoundingClientRect()`) right after changing a style forces the browser to lay out the page immediately. In a loop, that means one full layout per iteration:

```js
// Slow: read, write, read, write… forces a layout on every item
cards.forEach((card) => {
  card.style.height = `${card.offsetWidth * 0.75}px`;
});

// Fast: read everything first, then write everything
const widths = cards.map((card) => card.offsetWidth);
cards.forEach((card, i) => {
  card.style.height = `${widths[i] * 0.75}px`;
});
```

Often the CSS can do the job instead (here, `aspect-ratio: 4 / 3`), with no script at all.

### 5. Cut and delay third-party scripts

Third-party scripts hurt INP twice: they create input delay when their timers fire during an interaction, and many of them (analytics, heatmaps, tag manager click triggers) attach their own listeners to your clicks, adding processing time. Go through the third-party finding and, for each entry, ask what it gives you:

- **Nobody looks at it** (an old heatmap, a second analytics tool): remove it.
- **Needed, but not at load** (chat, reviews, video): load it on interaction or behind a facade, a static button or thumbnail that loads the real widget when clicked.
- **Needed at load** (consent, analytics): load it with `async` or `defer`, and keep tag manager tags to the ones in use.

The [third-party scripts learn page](https://getreport.app/learn/third-party-scripts) has more on facades.

### 6. Defer non-critical JavaScript and keep the DOM small

Scripts that run during the load compete with the first taps. Add `defer` to everything not needed for the first paint, and remove scripts from pages that do not use them. Large DOMs (the dom-size finding flags pages over 1,500 elements) make every style recalculation and layout slower, which lengthens the presentation phase of every interaction. Page builders nesting five wrappers per element and menus rendered twice (mobile and desktop) are the usual causes. For long pages, `content-visibility: auto` on below-the-fold sections lets the browser skip their rendering work until they are near the viewport.

If a developer does these fixes, send them the report link with the long-tasks and third-party findings; [how to send fixes to your developer](https://getreport.app/guides/send-fixes-to-your-developer) shows what to include.

## Platform notes

### WordPress

- **Page builders** (Elementor, Divi, WPBakery and similar) add large DOMs and a lot of JavaScript to every page. Use the builder's own performance settings (optimised DOM output, loading only the used widgets' assets) and avoid nesting sections inside sections.
- **Too many plugins**: each one that adds a script to every page adds main-thread work, even on pages where it does nothing. A script manager (Perfmatters, Asset CleanUp) can disable plugin scripts per page; the contact form's script only needs the contact page.
- **Tag managers**: Google Tag Manager tags fired on "All Clicks" run on every click. Audit the container and remove tags for tools you no longer use.
- **"Delay JavaScript execution"** in WP Rocket, Perfmatters and similar plugins holds scripts until the first interaction. It lowers TBT dramatically, but all delayed scripts then run the moment the visitor first taps, which can make that first interaction slow. Check the field INP after enabling it, and exclude scripts the first interaction needs (the menu, the cart).

### Shopify

Apps inject their scripts into every storefront page. Uninstall apps you no longer use, and check the theme code afterwards: some leave script tags behind in `theme.liquid`.

## Verify

- Re-run the lab test: TBT should be under 200 ms and the long-tasks list shorter.
- In DevTools with 4× CPU throttling, the interactions you fixed should show under 200 ms in the live metrics view.
- If you collect INP with `web-vitals`, the 75th percentile should fall within days.
- The field INP in the Core Web Vitals checker and Search Console follows over the next 28 days.

## Common mistakes

- **Testing on a fast laptop.** Symptom: every interaction feels instant to you, but field INP is poor. Fix: throttle the CPU 4× in DevTools; your visitors' phones are several times slower than your computer.
- **Only fixing load-time JavaScript.** Symptom: TBT is fine, INP still fails. Fix: find the slow interaction (menu, filter, search) and fix its handler.
- **Adding a spinner after the work.** Symptom: the loading state never appears. Fix: set it, yield, then do the work.
- **Delaying all JavaScript until interaction.** Symptom: great lab score, slow first tap. Fix: exclude the scripts that interaction needs, or defer them normally instead.
- **Measuring with heavy monitoring scripts.** Symptom: INP gets worse after adding a performance tool. Fix: use the small `web-vitals` library or rely on CrUX.
