What INP actually measures
In March 2024, Interaction to Next Paint (INP) became part of Google's Core Web Vitals, a set of metrics that report on user experience based on field data and are used in Google's search ranking. Modern frameworks like Next.js are optimized for good baseline INP scores, but the nature of the metric means that what you do in your app really matters for the outcomes. Many reasonable UX patterns actually push browsers to the limit of what they can render within the INP guidelines.
To understand why, it helps to dispel a common misconception: a page does not need to change visually for an interaction to count toward INP. "Paint" really just means the browser had the chance to paint. For example, a page with an empty click handler has perfect INP:
<button onClick={() => {}}>Click me</button>
Similarly, blocking the main thread for a full second within an event handler can still yield perfect INP if the code yields to the browser first:
<button
onClick={async () => {
await sleep(100);
blockTheMainThreadForOneSecond();
}}
>
Click me
</button>;
This code gives the browser time to paint via sleep(100), then blocks for a second. The INP metric does not penalize the blocking because the browser already had its chance to render. The same applies to this very reasonable-looking handler, which also blocks for a second but has perfect INP:
<button
onClick={async () => {
const data = await fetchData();
blockTheMainThreadForOneSecond(data);
}}
>
Click me
</button>;
However, this handler would have an INP of 1 second, which is considered very bad:
<html>
<button
onClick={() => {
blockTheMainThreadForOneSecond();
}}
>
Click me
</button>
</html>;
The key takeaway: optimizing INP is about arranging work so the browser gets a chance to paint and react to user action. It does not mean all work must be completed within the 200ms INP deadline.
Anatomy of an interaction
The time from interaction to next paint can be broken down into distinct phases:
- The user interaction starts the sequence.
- Other code (part 1): Your event handler might be blocked by other code running on the main thread.
- Event handler: Eventually your handler runs.
- Other code (part 2): After the handler, unrelated code can again block the main thread.
- Browser render: If the DOM changed, the browser re-renders it.
- Finally, paint occurs.
To optimize INP, each phase needs to be minimized.
The "other code" phase
In the web programming model, all code sharing a page defaults to a single thread and event loop, so unrelated code may interfere with handling a user event. This can be the hardest phase to optimize since it is not directly related to the code you care about. Third-party JavaScript is a common cause of delay here. When that is the culprit, the one thing you can do today is to call your vendor and ask them to fix their code. Vercel recently did this with analytics vendor Heap, which shipped a fix.
The event handler phase
This is your code reacting to user input. It might fetch data, change the DOM immediately, or do anything else. If it does change the DOM, the way it does so impacts the time the browser needs to render the new state. This combination of DOM change and render-time most directly impacts the INP score of the interaction.
The browser render phase
Here the browser turns changes from the previous phase into pixels. If nothing changed, this phase takes 0ms and paint is fast since there is nothing to paint. This phase is often empty — for example, when you must fetch data before rendering a response. That can lead to an ironic outcome: preloading data so you can respond immediately can make INP worse, because the render phase becomes non-empty and slow.
Optimizing the event handler
Event handlers themselves are often very fast. The primary cost in modern web applications is rerendering, including virtual DOM diffing on state changes. For React, classic optimizations like memo and immutable context and prop values are the best mechanisms to minimize this phase's duration.
Optimizing the browser render
This phase mostly lives in the browser's scope. Even though browser vendors ask web developers to "chunk their work more," browsers themselves still perform rendering monolithically. Most browser layout algorithms are O(n) in the size of the DOM that gets invalidated, so the job is to minimize the amount of DOM invalidated.
A difficult case from nextjs.org was the programming language picker in the documentation section:
From an INP perspective, this is essentially the worst case. Changing the language changes code contents, which changes the height of the box, which requires re-layout of the page. For a long documentation page, that is substantial work.
Optimizing this phase falls strictly into the "very advanced web development" category. Techniques include:
- Reducing DOM size; inlined SVGs can often be replaced with SVG images.
- Using animation-friendly techniques, such as only changing
opacity, which does not affect the layout tree and may run entirely on the GPU. - Using CSS containment, which lets the browser restrict layout to a contained area.
- Virtualizing long lists so actual DOM size is independent of list length.
A shippable alternative
What if things cannot be made fast enough? The language picker is a good example of an experience that will just be slower than 200ms on current-generation browsers with a sufficiently old mobile device. You could virtualize the document, but is that really better UX? Shouldn't browsers be good at viewing content?
It helps to remember what INP is about: acknowledging user input within 200ms. It does not expect the full response to be drawn within that time. If you needed to fetch data, you wouldn't be able to draw immediately anyway. The solution is to split the interaction into two phases: acknowledging the user interaction, and actually changing the page afterward.
Consider this LanguagePicker example, where selecting a new value adds a class to the select element and sets the new language:
import { useState } from 'react';
export function LanguagePicker({ setLanguage }) {
const [selected, setSelected] = useState();
return (
<select
className={selected ? `value-${selected}` : ''}
onChange={(e) => {
setSelected(e.target.value);
setLanguage(e.target.value);
}}
>
<option value="JS">JavaScript</option>
<option value="TS">TypeScript</option>
</select>
);
}
Setting the new language may be very expensive. To separate the acknowledgment from that expensive operation, Vercel shipped the await-interaction-response module:
pnpm add await-interaction-response
Here is how the module separates the two phases:
import { useState } from 'react';
import interactionResponse from 'await-interaction-response';
export function LanguagePicker({ setLanguage }) {
const [selected, setSelected] = useState();
return (
<select
className={selected ? `value-${selected}` : ''}
onChange={async (e) => {
setSelected(e.target.value);
await interactionResponse();
setLanguage(e.target.value);
}}
>
<option value="JS">JavaScript</option>
<option value="TS">TypeScript</option>
</select>
);
}
That single line ensures the acknowledgment of the user action happens immediately with minimal INP, while the expensive operation runs right after. Because a native select already acknowledges the user, the code can be simplified further:
import interactionResponse from 'await-interaction-response';
export function LanguagePicker({ setLanguage }) {
return (
<select onChange={async (e) => {
await interactionResponse();
setLanguage(e.target.value);
}}>
<option value="JS">JavaScript</option>
<option value="TS">TypeScript</option>
</select>
);
}
How it works
The implementation of await-interaction-response is very simple:
export function interactionResponse(): Promise<unknown> {
return new Promise((resolve) => {
setTimeout(resolve, 100); // Fallback for the case where the animation frame never fires.
requestAnimationFrame(() => {
setTimeout(resolve, 0);
});
});
}
The code waits for an animation frame and then also for a timeout. This allows the browser to paint the frame, then immediately run the code. The backup timeout covers the special case where the animation frame never runs, such as when the user moves to another tab at just the right instant.
While it first appears ironic to delay work, this approach:
- Gives the user immediate feedback that their action was accepted.
- Delays the response by a maximum of 1 frame, an average of 8ms — imperceptible for the major actions where you'd use this function.
An even simpler React approach
React offers a similar API, startTransition, which tells React that state updates in its callback do not need to be executed synchronously. If the slow operation is a result of a state change, adding startTransition is all that is needed:
import { startTransition } from "react";
export function LanguagePicker({ setLanguage }) {
return (
<select onChange={(e) => {
startTransition(() => {
setLanguage(e.target.value);
})
}}>
<option value="JS">JavaScript</option>
<option value="TS">TypeScript</option>
</select>
);
}
Finding what to optimize
Reproducing INP issues on powerful developer machines is challenging. Vercel Speed Insights now identifies the specific HTML elements impacting INP. These CSS selectors show exactly which elements on the page had slow interactions. When optimizing INP, go through this list by frequency, prioritizing the most frequent slow or improvable interactions:
The Vercel Toolbar also supports INP monitoring directly. It gives the CSS selector of impacted elements while actively previewing a page — in local dev, staging, or production — making it easy to see which component needs optimizing:
Pairing these tools with CPU throttling in the Chrome DevTools performance panel makes it easier to get results comparable to users on slower devices. One caution: CPU throttling sometimes yields extreme delays on the very first interaction, which is safe to ignore — just click again:
Summary
INP is often misunderstood as requiring the full user response to be painted within 200ms, when it is really about giving the user feedback that their input is being processed within that time. The most effective ways to improve INP are:
- Calling third-party code vendors, like analytics providers, and asking them to improve their event handling code.
- Optimizing JavaScript framework rendering performance with tools like React's
memo. - Helping the browser render more easily with techniques like CSS containment.
- Splitting event handling into two phases with
await-interaction-responseto ensure immediate acknowledgment for truly expensive page changes.



