A page can load quickly and still feel slow. A menu opens late, a filter hesitates, or a form appears frozen after a click. Interaction to Next Paint (INP) is the Core Web Vital designed to expose that gap between a user's action and the next visible response.

This guide gives you a repeatable way to diagnose and improve INP. You will learn what the metric includes, how to reproduce a slow interaction in Chrome DevTools, how to separate input delay from JavaScript work and rendering, and how to apply fixes without hiding the real bottleneck.

What INP actually measures

INP observes click, tap, and keyboard interactions throughout a page visit. For each interaction, the browser measures the time from the user's input until the next frame is painted. The page's INP is based on one of its slowest interactions, with adjustments intended to reduce the effect of unusual outliers on pages with many interactions.

An interaction has three useful phases:

  • Input delay: time spent waiting before the event callback can begin, often because another long task occupies the main thread.
  • Processing duration: time spent inside event callbacks and the work they trigger.
  • Presentation delay: time after callbacks finish but before the browser paints the next frame, including style, layout, and rendering work.

Google's current guidance considers an INP of 200 milliseconds or less good at the 75th percentile of page visits. Treat that threshold as a user-experience target, not permission to ignore interactions that are just below it.

Start with field data, then reproduce locally

Lab testing is controlled and debuggable, but it cannot represent every device, dataset, extension, and interaction sequence used in production. Begin with field data from a real-user monitoring system or the Chrome UX Report. Identify the affected URL, device class, and interaction type before opening DevTools.

In Chrome DevTools, open the Performance panel. Configure field data if it is available for the origin, select an appropriate device class, and use CPU throttling that resembles a mid-range device. Record while performing the specific interaction several times. Stop the recording only after the UI has visibly updated.

The Interactions track shows the event and its duration. Select a slow interaction, inspect its phase breakdown, and then follow the associated work on the Main thread. The longest phase tells you where to focus first.

Fix input delay by protecting the main thread

A click handler may be tiny and still start late. The usual cause is unrelated JavaScript already running when the user interacts. Large hydration tasks, analytics initialization, client-side rendering, and third-party scripts can all block input.

Break optional initialization into smaller tasks and schedule it after critical UI is usable. When a long loop can be processed incrementally, yield between chunks:

async function processRows(rows) {
  const batchSize = 100;

for (let index = 0; index < rows.length; index += batchSize) { updateSearchIndex(rows.slice(index, index + batchSize));

// Allow pending input and rendering to run between batches.
await new Promise((resolve) =&gt; setTimeout(resolve, 0));

} }

This is not a license to wrap every function in a timer. Split work at meaningful boundaries, keep state consistent between chunks, and prioritize the UI response users are waiting to see.

Reduce callback work in event handlers

Event handlers should do the minimum required to produce immediate feedback. Avoid synchronously filtering thousands of records, serializing large objects, or rendering an entire application subtree before the browser can paint.

Separate the immediate visual update from secondary work:

button.addEventListener("click", () => {
  button.disabled = true;
  status.textContent = "Saving…";

requestAnimationFrame(() => { void savePreferences().finally(() => { button.disabled = false; status.textContent = "Saved"; }); }); });

The first callback creates visible feedback. The following frame can begin before the asynchronous request proceeds. For CPU-heavy work that genuinely must remain on the client, consider a Web Worker so computation does not monopolize the main thread.

Avoid accidental synchronous work

Some browser APIs force pending layout calculations to complete immediately. A common pattern is writing styles and then reading geometry repeatedly:

// Avoid interleaving layout reads and writes in a loop.
for (const card of cards) {
  card.style.width = `${container.offsetWidth / 3}px`;
}

Read the measurement once, then perform the writes together:

const width = container.offsetWidth / 3;
for (const card of cards) {
  card.style.width = `${width}px`;
}

DevTools may label forced reflow activity in a performance trace. When presentation delay dominates, inspect style recalculation, layout, and paint events near the interaction.

Render less work after state changes

A small state change can cause a large framework component tree to render. Use your framework's profiler to confirm what rerenders, then narrow reactive state to the components that need it. Virtualize long lists, paginate expensive views, and avoid rebuilding stable objects on every render when that invalidates memoization.

CSS can also create expensive presentation work. Deep DOM trees, complex selectors, large blurred shadows, and animating layout properties increase style, layout, or paint cost. Prefer transform and opacity for motion when they provide the same visual result, and test on realistic hardware rather than relying on a fast development laptop.

Debounce carefully without making the interface feel broken

Debouncing is useful for work such as search requests, but delaying all feedback makes perceived responsiveness worse. Update the input and loading state immediately, then debounce only the expensive follow-up:

let searchTimer;

searchInput.addEventListener("input", (event) => { resultStatus.textContent = "Updating results…"; clearTimeout(searchTimer);

searchTimer = setTimeout(() => { void fetchResults(event.target.value); }, 200); });

If results can arrive out of order, pair the debounce with AbortController or a request identifier so stale responses cannot replace newer data.

Validate the fix instead of trusting one trace

Record the same scenario before and after the change under the same CPU and network settings. Compare the interaction phase breakdown, not only the total. Then ship the change gradually and watch field data. Core Web Vitals field datasets aggregate real visits, so improvement appears over time rather than instantly.

Also test keyboard input, touch interactions, long lists, logged-in states, and worst-case data. A fix that helps an empty demo may fail when a production account contains thousands of records.

Production checklist

  • Use field data to identify the URL, device class, and slow interaction.
  • Record that interaction in the Chrome DevTools Performance panel.
  • Determine whether input delay, processing, or presentation dominates.
  • Split long tasks and defer noncritical initialization.
  • Keep event callbacks focused on immediate feedback.
  • Batch DOM reads and writes to avoid forced synchronous layout.
  • Reduce unnecessary rendering and expensive visual effects.
  • Repeat the trace under identical throttling conditions.
  • Monitor real-user INP after deployment.

Related reading and official sources

If you work with server-rendered Vue applications, the site's guide to Nuxt 4 data fetching can help you avoid unnecessary duplicate requests and client work.

The measurement model and optimization recommendations in this tutorial follow web.dev's official guide to optimizing INP, the Chrome DevTools Performance panel documentation, and the Chrome runtime performance tutorial.