In March, Google released a new Core Web Vital called Interaction to Next Paint (INP). It measures the speed of interactions with the website. In this article, you’ll learn more about INP from the developer’s perspective. I’ll guide you through testing for INP, verifying potential causes of low INP scores, and how you can leverage new React tools to optimize your website for recent changes.
What is the “Interaction to Next Paint” (INP) metric?
It’s a scoring mechanism that analyzes latency from the time the user begins the interaction to the moment the next frame is presented with visual feedback. We’ll take a closer look at what mechanisms in modern applications cause low INP score later.
INP measures for one of the following interactions.
- Clicking with a mouse
- Tapping on a device with a touchscreen
- Pressing a key on either a physical or onscreen keyboard
For example, when the user types a character into the search input that filters a list of cats’ breeds, the INP value will be a duration from the moment of triggering a group of keyboard events (keydown, …, keyup) to the moment of re-rendering the updated list.
The main goal of INP is to minimize this duration, ensuring swift visual feedback for user interactions and optimizing overall responsiveness on the page.
What INP isn’t?
There is already a well-known metric called First Input Delay (FID) which measures the time from when a user first interacts with a page to the time when the main thread is next idle. It offered developers a new way to measure responsiveness as real users experience it.
FID vs INP - What’s the difference?
Comparing these two mechanisms reveals similar measurement goal - a page responsiveness. One of the essential differences is that the FID measures only initial interaction, while the INP takes the worst latency recorded during the entire session.
Even though first impressions are important, the first interaction is not necessarily representative of all interactions throughout the life of a page. This is one of the reasons why INP replaces the FID.
Should you care about INP?
Yes, if you care about SEO. The INP will be another metric that results will directly influence your website's ranking on Google search results.
If you don’t take SEO into account, consider leveraging INP mechanisms to track the responsiveness of your website. Receiving poor results from the INP might be a sign to profile your application for performance.
Measuring INP
For a released product, that runs on the production environment, accumulates user traffic, and qualifies for inclusion in the Chrome User Experience Report (CrUX) you can quickly measure the new INP metric using PageSpeed insights. It gives a snapshot of your page and origin's performance over the previous 28 days.
Manually diagnose slow interactions
Ideally, you should rely on the field data from the CrUX report, that suggests your website struggles with slow interactions. Sometimes, however, it’s not possible, and we need another way.
The main goal is to identify the problem with lowest amount of effort at first. For manual investigation, we’ll use the Web Vitals addon. Then, dive deeper into what causes the issue. For that, we can leverage the Profiler available in the browser’s DevTools tab.
Using Web Vitals addon (Chrome)
Requirements:
- Chrome browser
- Web Vitals addon
- React Developer Tools addon (Useful for highlighting component updates)
Let’s start with a simple configuration of the Web Vitals addon. This will display HUD overlay as well as log information about each interaction with the page in the console.
We’ll use React Developer Tools to highlight component updates. This will help us with identifying what’s happening in our application.
Let’s go back to previously introduced app which allows us to filter cats by their breeds. First issue we can observe is that the list itself is long. We’re displaying over 1900 results at once. That’s already something we could optimize, but let’s leave it for later. Let’s try to interact with the page by typing “B” into the input, and then clearing it.
Few things are happening here:
- A controlled Input component updates immediately on each character we type.
- A list updates on each input change.
The moment of “freeze” shown on the video is the main issue that causes INP score to suffer. We can verify that by observing the INP delta times logged in the console on the right. This translates into general poor result of the INP score for our website (344 ms).
What exactly causes poor INP results?
Figuring out what's causing poor INP is the most important, and the most difficult step on the road of enhancing the User Experience (UX).
As Google Chrome’s team suggest, interactions can be broken down into three phases:
1. The input delay
It starts when the user initiates an interaction with the page, and ends when the event callbacks for the interaction begin to run.
2. The processing time
It consists of the time it takes for event callbacks to run to completion.
3. The presentation delay
It’s the time it takes for the browser to present the next frame which contains the visual result of the interaction.
I found a bottleneck - now what? Optimizing your React app for performance.
As a React developer you’re probably aware of core concepts used to optimize the app.
I’d distinguish following:
- Memoization
memo
,useMemo
,useCallback
- Debouncing/Throttling (using a hook or a higher-order-function that triggers the callback)
- List virtualization
- List pagination
Let’s go back to our simple app about cats. Take a look at the component that provides browsing functionality:
Let’s see how can we utilize already known concepts to optimize the INP time.
Issue #1 - List of cats re-renders on each change of the SearchBar component.
As we know from the React’s Reconciliation model, when state changes, React re-renders the components and all its descendants. By calling setFilterByBreed
on each SearchBar change, we perform unnecessary re-renders of the expensive CatsList
component.
Instead of triggering onChange immediately, let’s apply debouncing on the SearchBar component.
You can use a library, or implement a simple debounce utility by yourself. Here’s a simple higher-order-function that triggers callback
on timeout:
A SearchBar
component is a simple wrapper around mui’s TextField
component:
Using debounced onChange
:
This improves the INP, because we no longer try to re-render expensive component on each user interaction.
We still however, render a long list of elements in the DOM, synchronously.
Issue #2- List renders all elements at once causing low INP score by freezing the layout
This time we could:
a) Virtualize the list (by limitting amount of elements attached to the DOM tree, and displaying only such that are visible in the viewport + some offset).
b) Paginate the data (combine intersection-observer with appending next elements to the DOM on user scroll).
Both solutions allow us to keep initial list lightweight, and get rid of the layout “freeze” that blocks us from interacting with the website. There’s a plenty of resources for implementing both Virtualization, and Pagination in React. You can check one of them.
React ≥18, concurrency, and the new mental model of reactivity
Apart from what I already mentioned, there’s a relatively new approach to reactivity in React using its latest features. By introducing concurrency, we’re now able to transition (no pun intended) from well-known optimization techniques, take the responsibility from the developer, and introduce less intrusive optimizations managed by the tool itself (with a bit of our help).
Non-urgent updates
With React < 18, every update is urgent. From React 18, every update is urgent by default, but now we’re able to set a low priority on updates that could potentially block the UI.
Instead of thinking about re-renders, we should start thinking about the urgency of updates we perform.
Let’s once again go back to our unoptimized cats browser (Fig. 1.)
Remember the part (Fig. 1. [8], Fig. 5. [1]) where, we update the state of the filterByBreed
on each SearchBar change? We’ll utilize the useTransition
hook to mark this as non-urgent update.
As simple as that, we were able to eliminate the “freezing” UI issue.
Why this works?
Under the hood, react maintains a queue of updates ready to be performed. Before React 18, all updates were performed sequentially until there were no more updates left. When we update the component’s state, React will queue this update, and perform it until it completes. This however, due to the JavaScript event loop’s Run-to-completion strategy, blocks processing of other tasks (that’s why we can see the UI “freeze” with non-optimized example).
With the release of React 18, a significant change introduced a new mechanism of processing the updates queue. Instead of forcefully processing an update, React now schedules a unit of update’s work, checks if the browser allows to perform it, and then completes it. Otherwise, it gives the control back to the browser. This way, long-running non-urgent updates no longer blocks the browser’s work that needs to be performed due to the higher priority.
Transition is only one of the concurrent scheduling mechanisms. Make sure to check all already-existing, and incoming hooks such as useDeferredValue, or use.
Is concurrent scheduling a solution to all problems?
While the new concurrency model effectively addresses common challenges, it's not a silver bullet applicable to every aspect of your code.
Since useTransition
splits the work in chunks, for expensive-to-render components, splitting the work is impossible. That’s where we need to combine the new concurrent scheduling with already known optimization strategies.
Avoid premature optimization
Understanding the optimization techniques mentioned and comprehending how the INP calculates the Web Vitals score is crucial for delivering a seamless and responsive user experience for your product. While optimization enhances performance, it often comes with associated costs. While it's tempting to immediately implement efficiency improvements, many enhancements result in higher memory consumption and maintenance challenges. It is essential to prioritize simplicity in our solutions. I advise vigilant monitoring of your application to identify areas causing issues and optimizing those selectively, rather than prematurely optimizing, which can introduce unnecessary complexity to your project.