1. Introduction
An interactive demo and codelab for learning about Interaction to Next Paint (INP).
Prerequisites
- Knowledge of HTML and JavaScript development.
- Recommended: read the INP documentation.
What you learn
- How the interplay of user interactions and your handling of those interactions affect page responsiveness.
- How to reduce and eliminate delays for a smooth user experience.
What you need
- A computer with the ability to clone code from GitHub and run npm commands.
- A text editor.
- A recent version of Chrome for all the interaction measurements to work.
2. Get set up
Get and run the code
The code is found in the the web-vitals-codelabs repository.
- Clone the repo in your terminal:
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git - Traverse into the cloned directory:
cd web-vitals-codelabs/understanding-inp - Install dependencies:
npm ci - Start the web server:
npm run start - Visit http://localhost:5173/understanding-inp/ in your browser
Overview of the app
Located at the top of the page is a Score counter and Increment button. A classic demo of reactivity and responsiveness!

Below the button there are four measurements:
- INP: the current INP score, which is typically the worst interaction.
- Interaction: the score of the most recent interaction.
- FPS: the main thread frames-per-second of the page.
- Timer: a running timer animation to help visualize jank.
The FPS and Timer entries are not at all necessary for measuring interactions. They are added just to make visualizing responsiveness a little easier.
Try it out
Try to interact with the Increment button and watch the score increase. Do the INP and Interaction values change with each increment?
INP measures how long it takes from the moment the user interacts until the page actually shows the rendered update to the user.
3. Measuring interactions with Chrome DevTools
Open DevTools from the More Tools > Developer Tools menu, by right clicking on the page and selecting Inspect, or by using a keyboard shortcut.
Switch to the Performance panel, which you'll use to measure interactions.

Next, capture an interaction in the Performance panel.
- Press record.
- Interact with the page (press the Increment button).
- Stop the recording.
In the resulting timeline, you'll find an Interactions track. Expand it by clicking on the triangle on the left hand side.

Two interactions appear. Zoom in on the second one by scrolling or holding the W key.

Hovering over the interaction, you can see the interaction was fast, spending no time in processing duration, and a minimum amount of time in input delay and presentation delay, the exact lengths of which will depend on the speed of your machine.
4. Long-running event listeners
Open the index.js file, and uncomment the blockFor function inside the event listener.
See full code: click_block.html
button.addEventListener('click', () => {
blockFor(1000);
score.incrementAndUpdateUI();
});
Save the file. The server will see the change and refresh the page for you.
Try interacting with the page again. The interactions will now be noticeably slower.
Performance trace
Take another recording in the Performance panel to see what this looks like there.

What was once a short interaction now takes a full second.
When you hover over the interaction, notice the time is almost entirely spent in "Processing duration", which is the amount of time taken to execute the event listener callbacks. Since the blocking blockFor call is entirely within the event listener, that's where the time goes.
5. Experiment: processing duration
Try out ways of rearranging the event-listener work to see the effect on INP.
Update UI first
What happens if you swap the order of js calls—update the UI first, then block?
See full code: ui_first.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
blockFor(1000);
});
Did you notice the UI appear earlier? Does the order affect INP scores?
Try taking a trace and examining the interaction to see if there were any differences.
Separate listeners
What if you move the work to a separate event listener? Update the UI in one event listener, and block the page from a separate listener.
See full code: two_click.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('click', () => {
blockFor(1000);
});
What does it look like in the performance panel now?
Different event types
Most interactions will fire many types of events, from pointer or key events, to hover, focus/blur, and synthetic events like beforechange and beforeinput.
Many real pages have listeners for many different events.
What happens if you change the event types for the event listeners? For example, replace one of the click event listeners with pointerup or mouseup?
See full code: diff_handlers.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('pointerup', () => {
blockFor(1000);
});
No UI update
What happens if you remove the call to update UI from the event listener?
See full code: no_ui.html
button.addEventListener('click', () => {
blockFor(1000);
// score.incrementAndUpdateUI();
});
6. Processing duration experiment results
Performance trace: update UI first
See full code: ui_first.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
blockFor(1000);
});
Looking at a Performance panel recording of clicking the button, you can see that the results did not change. While a UI update was triggered before the blocking code, the browser didn't actually update what was painted to screen until after the event listener was complete, which means the interaction still took just over a second to complete.

Performance trace: separate listeners
See full code: two_click.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('click', () => {
blockFor(1000);
});
Again, there's functionally no difference. The interaction still takes a full second.
If you zoom way into the click interaction, you'll see that there are indeed two different functions being called as a result of the click event.
As expected, the first—updating the UI—runs incredibly quickly, while the second take a full second. However, the sum of their effects results in the same slow interaction to the end user.

Performance trace: different event types
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('pointerup', () => {
blockFor(1000);
});
These results are very similar. The interaction is still a full second; the only difference is that the shorter UI-update-only click listener now runs after the blocking pointerup listener.

Performance trace: no UI update
See full code: no_ui.html
button.addEventListener('click', () => {
blockFor(1000);
// score.incrementAndUpdateUI();
});
- The score doesn't update, but the page still does!
- Animations, CSS effects, default web component actions (form input), text entry, text highlighting all continue to update.
In this case the button goes to an active state and back when clicked, which requires a paint by the browser, which means there's still an INP.
Since the event listener blocked the main thread for a second preventing the page from being painted, the interaction still takes a full second.
Taking a Performance panel recording shows the interaction virtually identical to those that came before.

Takeaway
Any code running in any event listener will delay the interaction.
- That includes listeners registered from different scripts and framework or library code that runs in listeners, such as a state update that triggers a component render.
- Not only your own code, but also all third party scripts.
It's a common problem!
Finally: just because your code doesn't trigger a paint doesn't mean a paint won't be waiting on slow event listeners to complete.
7. Experiment: input delay
What about long running code outside of event listeners? For example:
- If you had a late-loading
<script>that randomly blocked the page during load. - An API call, such as
setInterval, that periodically blocks the page?
Try removing the blockFor from the event listener and adding it to a setInterval():
See full code: input_delay.html
setInterval(() => {
blockFor(1000);
}, 3000);
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
What happens?
8. Input delay experiment results
See full code: input_delay.html
setInterval(() => {
blockFor(1000);
}, 3000);
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
Recording a button click that happens to occur while the setInterval blocking task was running results in a long-running interaction, even with no blocking work being done in the interaction itself!
These long-running periods are often called long tasks.
Hovering over the interaction in DevTools, you'll be able to see the interaction time is now primarily attributed to input delay, not processing duration.

Notice, it doesn't always affect the interactions! If you don't click when the task is running, you may get lucky. Such "random" sneezes can be a nightmare to debug when they only sometimes cause issues.
One way to track these down is through measuring long tasks (or Long Animation Frames), and Total Blocking Time.
9. Slow presentation
So far, we've looked at the performance of JavaScript, via input delay or event listeners, but what else affects rendering next paint?
Well, updating the page with expensive effects!
Even if the page update comes quickly, the browser may still have to work hard to render them!
On the main thread:
- UI frameworks that need to render updates after state changes
- DOM changes, or toggling many expensive CSS query selectors can trigger lots of Style, Layout, and Paint.
Off the main thread:
- Using CSS to power GPU effects
- Adding very large high-resolution images
- Using SVG/Canvas to draw complex scenes

Some examples commonly found on the web:
- An SPA site that rebuilds the entire DOM after clicking a link, without pausing to provide an initial visual feedback.
- A search page that offers complex search filters with a dynamic user interface, but runs expensive listeners to do so.
- A dark mode toggle that triggers style/layout for the whole page
10. Experiment: presentation delay
Slow requestAnimationFrame
Let's simulate a long presentation delay using the requestAnimationFrame() API.
Move the blockFor call into a requestAnimationFrame callback so it runs after the event listener returns:
See full code: presentation_delay.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
blockFor(1000);
});
});
What happens?
11. Presentation delay experiment results
See full code: presentation_delay.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
requestAnimationFrame(() => {