---
title: "How to Improve INP: A Step-by-Step Guide | Evaluat"
description: "Improve INP by fixing your slowest interaction. Break up long tasks, yield to the main thread, trim handlers, and tame hydration, with code and 2025 data."
url: "https://www.evaluat.com/blog/how-to-improve-inp"
last_modified: "2026-07-18"
---

[Blog](https://www.evaluat.com/blog) [Web Vitals & Metrics](https://www.evaluat.com/blog/category/web-vitals-metrics)

# How to improve INP: a step-by-step optimization guide

Most teams trying to improve INP optimize the click handler, and most of the time the handler is not the problem. A long task was already running on the main thread when the user clicked, so the interaction had to wait. This guide is the practical playbook: the fixes in priority order, the code for each, the modern API most sites have not adopted yet, and how to confirm the number actually moved for real users under load.

Written by: [Ahmad Farzan](https://www.evaluat.com/authors/ahmad-farzan) · 18 June 2026 · Updated 18 July 2026

![Two Interaction to Next Paint timelines compared. Before optimization a long task creates a large input delay and INP is 520 milliseconds, rated poor. After breaking up the task and yielding to the main thread the input delay nearly disappears and INP drops to 180 milliseconds, under the 200 millisecond good threshold.](https://www.evaluat.com/blog/how-to-improve-inp-cover.svg)

Summary

To improve Interaction to Next Paint, or INP, find your slowest interaction, split its latency into three phases, input delay, processing duration, and presentation delay, and fix the phase that owns the most time. Most teams tune the click handler, and most of the time the handler isn't the problem: a long task was already running on the main thread when the user clicked, so the interaction had to wait before the handler could even start. That's why the highest-leverage fix is breaking long tasks into smaller pieces and yielding to the main thread between them, ideally with the browser's newer scheduler yield API and a setTimeout fallback for browsers that don't support it yet. It works: the retailer Trendyol used exactly that pattern to cut its mobile INP in half, from nearly a second to around half a second. Responsiveness is largely a mobile problem, with seventy-seven percent of mobile sites passing INP against ninety-seven percent on desktop in the 2025 Web Almanac. The rest of the playbook: trim event handlers so only the visual-critical work runs synchronously, audit and defer third-party scripts, batch DOM reads and writes, and suspect hydration when the first tap is the slowest. Then verify the fix in the field, at the 75th percentile rather than the average, because INP reports your slowest interactions, and a lab tool can't measure it on a cold load at all.

Listen to this article · 1:38

[Download the audio summary (MP3)](https://www.evaluat.com/blog/audio/how-to-improve-inp.mp3)

To improve Interaction to Next Paint, find your slowest interaction, split its latency into three phases, and fix the phase that owns the most time. A frequent, high-leverage offender is a long task that was already running on the main thread when the user clicked: the interaction has to wait for that task to finish before the handler can even start. The fix there is rarely a faster handler. It is a thread that was free when the click arrived.

Responsiveness is largely a mobile problem. In the [2025 Web Almanac](https://almanac.httparchive.org/en/2025/performance), 77% of mobile websites had good INP against 97% on desktop, up from 74% on mobile the year before (HTTP Archive, 2025). The gap is CPU: a phone runs the same JavaScript several times slower, so the same long task blocks the same click for longer.

This guide is the practical playbook: the fixes in priority order, the code for each, the modern API most teams have not turned on yet, and how to confirm the number actually moved for real users. If you want the concept first, what counts as an interaction and why INP replaced First Input Delay, start with [Interaction to Next Paint, explained](https://www.evaluat.com/blog/interaction-to-next-paint). This is the how, not the what.

## First, find the slow interaction and split its time

Every INP fix starts with a measurement, not a guess: which interaction is your slowest, and which of its three phases owns the most time. INP is the sum of three sequential phases, defined in [web.dev’s guide to optimizing INP](https://web.dev/articles/optimize-inp) (web.dev, 2025): the **input delay**, from the moment the user acts until the event callbacks begin to run; the **processing duration**, the time those callbacks take to finish; and the **presentation delay**, the time for the browser to paint the next frame showing the result.

Find your slowest interaction in the field, because that is what INP reports. Google’s `web-vitals` library has an attribution build that names the culprit: the target element, the phase breakdown, and the event type.

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

onINP(({ value, attribution }) => {
  // value is the INP in ms; attribution says which interaction and which phase
  console.log(value, attribution.interactionTarget);
  console.log(attribution.inputDelay, attribution.processingDuration, attribution.presentationDelay);
});
```

Locally, Chrome DevTools shows the same split: record in the Performance panel, trigger the interaction, and the Interactions track breaks it into the three phases and points at the long tasks in the way. One caveat shapes everything below: a cold page load has no interaction, so [lab tools cannot measure INP from a standard run](https://www.evaluat.com/blog/interaction-to-next-paint). You measure it in the field, or with scripted interactions that perform the actual clicks.

The fixes below are ordered by leverage, cheapest and highest-payoff first. Apply the ones that target your largest phase, since the breakdown differs from page to page.

| Fix, in order                         | The phase it attacks            | The change                                                 | Effort vs payoff                      |
| ------------------------------------- | ------------------------------- | ---------------------------------------------------------- | ------------------------------------- |
| Break up long tasks, yield the thread | Input delay                     | Split work into chunks, `scheduler.yield()` between them   | Low effort, usually the biggest win   |
| Trim the event handler                | Processing duration             | Do the visual-critical work now, defer the rest            | Medium effort, real on heavy handlers |
| Audit and defer third-party scripts   | Input delay and processing      | Remove, defer, or facade analytics, consent, and chat tags | Medium effort, often overlooked       |
| Cut presentation delay                | Presentation delay              | Batch DOM reads and writes, shrink the DOM                 | Medium effort, smaller but real       |
| Fix the hydration trap                | Input delay, first interactions | Ship less JS, code-split, hydrate progressively            | Higher effort, fixes early taps       |

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

This is the highest-leverage fix, and the one most pages need. [A long task is any task that runs longer than 50 milliseconds](https://web.dev/articles/optimize-long-tasks) (web.dev, 2025), and while it runs the main thread is blocked, so a click that arrives mid-task sits in input delay until the task finishes. Break the long task into smaller pieces and hand control back to the browser between them, and a waiting interaction can run in the gap.

The modern way to yield is `scheduler.yield()`. The detail that matters is what it does that an old `setTimeout(0)` does not: [`scheduler.yield()` resumes your work at the front of the queue](https://developer.chrome.com/blog/use-scheduler-yield) (Chrome for Developers, 2025), so you give the browser a chance to handle a pending interaction without sending the rest of your own work to the back of the line.

```js
async function saveSettings() {
  const tasks = [validateInput, persistToStore, renderTable, sendAnalytics];

  for (const task of tasks) {
    task();
    // hand control back so a waiting click can run, then resume
    await yieldToMain();
  }
}

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

`scheduler.yield()` is not yet Baseline. As of 2026 it ships in Chrome and Edge 129 and Firefox 142, but [Safari does not support it](https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/yield) (MDN, 2026), which is why the `setTimeout` fallback above matters: it yields too, just without the front-of-queue priority. **Verify it worked** by recording the interaction again in DevTools. The single long task should now be several short ones, with the interaction running between them.

The payoff is not hypothetical. Trendyol traced a slow interaction to a single 700 to 900 millisecond long task in an Intersection Observer callback, deferred it with exactly this `scheduler.yield()` and `setTimeout` pattern, and [cut their mobile INP by 50%, from 963 ms to around 481 ms](https://web.dev/case-studies/trendyol-inp), which lifted click-through by 1% (web.dev, 2023).

## Step 2: trim the work in the event handler

When the processing duration is the largest phase, the handler is doing too much synchronously. The goal is to do only the work that produces the frame the user is waiting for, and defer everything else until after that frame paints.

Separate the urgent from the rest. Update the thing the user touched now, then push analytics, prefetching, and non-visual state into a later task with `scheduler.postTask()` or a `requestAnimationFrame` followed by a timeout. In React, [mark the non-urgent update as a transition](https://react.dev/reference/react/startTransition) (React, 2026) with `startTransition` or `useDeferredValue`, so the framework keeps the UI responsive mid-render instead of blocking on a large re-render:

```jsx
import { startTransition, useState } from 'react';

function handleChange(value) {
  setQuery(value);                  // urgent: the input reflects the keystroke now
  startTransition(() => {
    setResults(search(value));      // non-urgent: React can interrupt this for new input
  });
}
```

Avoid the trap of one giant click handler that validates, mutates state, triggers a synchronous re-render, and logs, all before the browser can paint. Each of those is a candidate to defer.

## Step 3: audit and defer third-party scripts

Third-party scripts are a major source of the long tasks that inflate input delay, because analytics, consent, A/B, and chat tags often attach listeners that run on every interaction. The scale is easy to underestimate: in the [2024 Web Almanac](https://almanac.httparchive.org/en/2024/javascript), the median mobile page already spent 2,366 ms in long tasks, and the slowest quarter spent over 4,400 ms (HTTP Archive, 2024). A measurable share of that is not your code.

Audit it in the DevTools Performance panel: attribute each long task to its source script, and you will usually find a tag you can defer, lazy-load behind a facade, or remove outright. The worst offenders tend to be the most common ones. In the [2022 Web Almanac](https://almanac.httparchive.org/en/2022/third-parties), Google Tag Manager blocked the main thread on 75% of the mobile pages that included it, and Google Analytics on 70% (HTTP Archive, 2022). Load what you can after the page is interactive, and gate heavy embeds such as chat, video, and maps behind a click-to-load placeholder so they never run during the first, busiest seconds.

## Step 4: cut presentation delay

The handler has run, but the frame has not painted. A long presentation delay usually means the interaction forced expensive layout or rendering work before the browser could draw.

The classic cause is layout thrashing: reading a layout property such as `offsetWidth`, writing to the DOM, then reading again, which forces the browser to recalculate layout repeatedly within a single frame. Batch your reads, then your writes, so the browser lays out once. Beyond that, a large or deeply nested DOM makes every recalculation slower, so trimming the node count helps directly, and CSS `content-visibility: auto` lets the browser skip rendering work for off-screen content. Set `width` and `height` on images and media so a late-arriving asset does not reflow the page after the interaction lands.

## Step 5: fix the hydration trap

The first four steps assume the page is loaded and idle. The hardest INP problems happen earlier, in the seconds after a server-rendered page appears but before it is interactive. The HTML paints fast, so the page looks ready, but the framework is busy on the main thread attaching event listeners and rebuilding state, a step called hydration. A tap in that window sits in a long input delay. As [web.dev’s guide to rendering on the web](https://web.dev/articles/rendering-on-the-web) puts it, server-side rendering with rehydration can have a significant negative impact on INP even when it improves first paint (web.dev, 2026).

There is no one-line fix, because the cause is the amount of JavaScript you ship and execute. The levers are structural: send less JavaScript, code-split so a route hydrates only what it shows, and hydrate progressively or lazily so below-the-fold and non-interactive components do not compete for the main thread on load. Architectures that hydrate selectively, such as islands, exist specifically to keep this window short. If your INP is worst on the first interaction and fine afterward, suspect hydration before you tune a handler.

## Verify the fix held: at the 75th percentile, and under load

A fix you cannot measure is a guess. After each change, confirm INP moved where it counts: in the field, at the 75th percentile, and under realistic concurrency.

Report field INP at the 75th percentile, not the mean, because [the published good threshold is 200 ms at the 75th percentile](https://web.dev/articles/inp) (web.dev, 2025) and an average hides your slowest interactions, which are exactly the ones INP exists to catch. Read it from the Chrome User Experience Report or from the `web-vitals` attribution build wired into your analytics.

Two readings will disagree, and that is expected. A lab tool runs one interaction on one controlled machine, while the field reflects a distribution of real devices, networks, and main-thread workloads. [Core Web Vitals: why lab scores differ from real users](https://www.evaluat.com/blog/core-web-vitals-lab-vs-field) explains which to trust for what. A single run also cannot load shared backends, CDNs, and third parties. Under concurrency those services can queue work, delaying hydration or network-bound interactions in each independent browser. Reproducing that needs real browsers performing real interactions while the shared system is under load, which is the subject of [measuring Core Web Vitals at load](https://www.evaluat.com/blog/core-web-vitals-load-testing). To stop a fix from quietly regressing later, use existing CI tooling as covered in [performance regression testing](https://www.evaluat.com/blog/performance-regression-testing); Evaluat’s managed Testing Suite and CI gates are planned.

## Common mistakes when improving INP

The same handful of errors waste most INP effort. Each has a one-line fix.

- **Tuning the handler when the delay is elsewhere.** If the time is in input delay, a faster handler changes nothing. Split the interaction into phases before you optimize.
- **Reporting the average.** The mean hides your slowest interactions, which are what INP measures. Track and gate on the 75th percentile.
- **Testing one idle user.** A single interaction does not stress shared backends or third parties. Independent devices do not share a main thread, but peak latency in shared services can still delay each browser’s interaction.
- **Only testing on a fast laptop.** INP is largely a mobile problem, 77% of mobile sites passing against 97% on desktop in the [2025 Web Almanac](https://almanac.httparchive.org/en/2025/performance). Test with mobile-class CPU throttling.
- **Micro-optimizing your code while a tag dominates.** A third-party script can own more main-thread time than anything you wrote. Audit before you refactor.
- **Sprinkling `scheduler.yield()` everywhere.** Yielding has a cost and only helps where a long task blocks an interaction. Find the long task, then break it up.

## Where Evaluat fits

Most of the steps above use free tools: DevTools to split an interaction into its phases, the `web-vitals` library to read field INP at the 75th percentile, your framework’s profiler for handler work. Use them. They are the right tools for one page and one developer.

[Evaluat](https://www.evaluat.com/product/performance-testing) is built for the dimension those miss: what your INP does when real traffic is interacting at once. Each virtual user runs in its own isolated browser and performs the real clicks and taps of the journey, so the INP recorded is the browser’s native value, captured at the concurrency you expect. When an interaction stalls at peak, the per-session network and console logs show what the main thread and backend were doing at that moment, and the session video shows the click that waited, so a long input delay and a slow handler read as the two different problems they are. [How it works](https://www.evaluat.com/how-it-works) covers the architecture.

One honest boundary: INP is a browser metric. If you are load-testing a pure API with no interface to interact with, a protocol tool like [k6](https://www.evaluat.com/vs/k6) is the lighter, cheaper fit. Evaluat earns its place when the question is what a controlled real browser records under load; field data remains the source for what the full user population experienced.

Improving INP is not one trick. Find your slowest interaction, split it into input delay, processing, and presentation, and fix the largest phase, which is most often a long task standing between the click and the handler. Then prove it held in the field, at the 75th percentile, under the load your users actually bring.

Test in real browsers. Debug in real sessions. [Book a demo](https://www.evaluat.com/demo?intent=performance).

![Ahmad Farzan, Founder at Evaluat](https://www.evaluat.com/authors/ahmad-farzan.jpg)

About the author

[Ahmad Farzan](https://www.evaluat.com/authors/ahmad-farzan) · Founder at Evaluat

Founder of Evaluat. Has spent years building and load-testing Adobe Commerce and Magento storefronts, and built Evaluat to test sites the way real browsers actually hit them.

[Ahmad Farzan on LinkedIn](https://www.linkedin.com/in/ahmadfarzan/) [Ahmad Farzan on GitHub](https://github.com/farzanahmad)

[More from Ahmad →](https://www.evaluat.com/authors/ahmad-farzan)

Common questions

## FAQ

### What is the fastest way to improve INP?

Break up the long task that blocks your slowest interaction and yield to the main thread between the pieces. On most pages a click arrives while a long task is still running, so the handler cannot start until the thread is free. Yielding lets the browser run the waiting interaction in the gap, which usually moves INP more than tuning the handler itself.

### Why did my INP not improve after I optimized the event handler?

Because the handler was probably not the bottleneck. INP has three phases, and the time is often lost in the input delay, before your handler runs, while a long task or a third-party script holds the main thread. Split your slowest interaction into input delay, processing, and presentation, then fix the phase that owns the most time.

### How do I prioritize which INP fix to apply first?

Measure your slowest interaction, split it into input delay, processing duration, and presentation delay, and attack the largest phase. A long input delay points to main-thread contention, so break up long tasks first. Heavy processing points to the handler and framework re-renders. A long presentation delay points to layout and rendering work.

### Does scheduler.yield() work in every browser?

Not yet. As of 2026 scheduler.yield() ships in Chrome and Edge 129 and Firefox 142, but not Safari, which is why MDN lists it as limited availability. Feature-detect it and fall back to setTimeout so every browser still yields. The fallback sends the rest of your work to the back of the task queue rather than the front, but it keeps the page responsive.

### How do I improve INP in a React or Next.js app?

Mark non-urgent state updates as transitions with startTransition or useDeferredValue so React keeps the UI responsive mid-render, and do only the visual-critical work in the handler itself. Reduce the JavaScript you ship and hydrate, because hydration keeps the main thread busy after the page looks ready, a common cause of slow first interactions.

### How do I confirm INP improved for real users?

Read field INP from the Chrome User Experience Report or the web-vitals library at the 75th percentile, not as an average, because INP reports your slowest interactions. Lab tools cannot measure INP on a cold load because there is no interaction to time, so use scripted interactions while shared backends and third parties are under realistic concurrency.

Related reading

## More from the blog

[![Interaction to Next Paint: a visit's interactions drawn as stacked bars of input delay, processing, and presentation time. Most stay under Google's 200ms good threshold while one slow interaction passes 500ms and sets the page's INP.](https://www.evaluat.com/blog/interaction-to-next-paint-cover.svg)](https://www.evaluat.com/blog/interaction-to-next-paint)

### [Interaction to Next Paint (INP), explained for engineers](https://www.evaluat.com/blog/interaction-to-next-paint)

[A page can pass every functional test and still feel slow on the second tap. Interaction to Next Paint is the Core Web Vital that catches it: the latency of your slowest interaction across a visit, timed from the click to the next frame painted. Here is what INP captures, what drags it past 200ms, and how to test it under load.](https://www.evaluat.com/blog/interaction-to-next-paint)

[Ahmad Farzan · 30 May 2026](https://www.evaluat.com/blog/interaction-to-next-paint)

[![Core Web Vitals at load: a page holds a good 2.1 second Largest Contentful Paint for one user, but as concurrent virtual users rise, LCP climbs and crosses the 2.5 second good threshold, ending near 3.4 seconds at 500 users.](https://www.evaluat.com/blog/core-web-vitals-load-testing-cover.svg)](https://www.evaluat.com/blog/core-web-vitals-load-testing)

### [Core Web Vitals at load, explained](https://www.evaluat.com/blog/core-web-vitals-load-testing)

[A page can score green in a single-user Lighthouse run and still ship a red Largest Contentful Paint the moment real traffic arrives. Core Web Vitals change under load: the server slows, time to first byte grows, and interactions wait on a busy backend. This guide explains why each Vital moves under load, and how to measure them at concurrency.](https://www.evaluat.com/blog/core-web-vitals-load-testing)

[Ahmad Farzan · 1 June 2026](https://www.evaluat.com/blog/core-web-vitals-load-testing)

[![Core Web Vitals lab versus field data: a lab test is one synthetic point that scores good, while real-user field data is a wide distribution whose 75th percentile crosses the good threshold and fails, because real devices, networks, and traffic vary.](https://www.evaluat.com/blog/core-web-vitals-lab-vs-field-cover.svg)](https://www.evaluat.com/blog/core-web-vitals-lab-vs-field)

### [Core Web Vitals: why lab scores differ from real users](https://www.evaluat.com/blog/core-web-vitals-lab-vs-field)

[Your Lighthouse score says 98. Your Core Web Vitals report says the page is failing. Both can be right. A lab test measures one synthetic load on a fixed device and network; field data is the spread of every real device, connection, and click your users bring, including the traffic a lab never simulates. Here is why lab and field diverge, and which to trust.](https://www.evaluat.com/blog/core-web-vitals-lab-vs-field)

[Ahmad Farzan · 13 May 2026](https://www.evaluat.com/blog/core-web-vitals-lab-vs-field)

See it on your site

Test in real browsers.\
Debug in real sessions.

## Start with one real-browser page load.

Pulse loads a public URL once, returns LCP and CLS plus FCP and TTFB, grades the page, and keeps a video on a shareable link.

[Run a free speed test](https://www.evaluat.com/tools/website-speed-test) [Book a demo](https://www.evaluat.com/demo?intent=performance)
