Understanding Interaction to Next Paint (INP)

1. Introduction

An interactive demo and codelab for learning about Interaction to Next Paint (INP).

A diagram depicting an interaction on the main thread. The user makes an input while blocking tasks run. The input is delayed until those tasks complete, after which the pointerup, mouseup, and click event listeners run, then rendering and painting work is kicked off until the next frame is presented

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.

  1. Clone the repo in your terminal: git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
  2. Traverse into the cloned directory: cd web-vitals-codelabs/understanding-inp
  3. Install dependencies: npm ci
  4. Start the web server: npm run start
  5. 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!

A screenshot of the demo app for this codelab

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.

A screenshot of the DevTools Performance panel alongside the app

Next, capture an interaction in the Performance panel.

  1. Press record.
  2. Interact with the page (press the Increment button).
  3. 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.

An animated demonstration of recording an interaction using the DevTools performance panel

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

A screenshot of the DevTools Performance panel, the cursor hovering over the interaction in the panel, and a tooltip listing the short timing of the interaction

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.

A one-second-long interaction in the Performance panel

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.

A still one-second-long interaction in the Performance panel

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.

A zoomed-in look at the one-second-long interaction in this example, showing the first function call taking less than a millisecond to complete

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.

A zoomed-in look at the one-second-long interaction in this example, showing the click event listener taking less than a millisecond to complete, after the 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.

A still one-second-long interaction in the Performance panel

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.

The DevTools Performance panel showing an one-second blocking task, an interaction coming in part way through that task, and a 642 millisecond interaction, mostly attributed to input delay

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

Sketch of the different elements of rendering on the web

RenderingNG

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(() => {