1. Introduction to NextPaint Metric
Let's start with two bad news. First, there are no Rack Server components in this talk. Second, the interaction to NextPaint metric is a new performance metric introduced by Google. It measures how fast clicks or keyboard input are on the page. This metric becomes a core vital in March 2024. Let's dive into theory by covering the interaction, the interaction to NextPaint, and the interaction to NextPaint for the page. In a React app, the filtering notes interaction is slow, causing the spinner to freeze for a second.
All right, so let's start the talk with two bad news. The first one is that if you came to the talk for Rack Server components, there are not going to be any Rack Server components in this talk. They did not fit in. The second bad news is that we, as Rack developers, are bleeped. They would ban me otherwise if I said the word. But this is the PageSpeed Insights Report for newyorktimes.com. You can see that all of these performance metrics are green except the interaction to NextPaint.
This is the PageSpeed Insights Report for target.com, a big American e-commerce written in React just like newyorktimes.com. All PageSpeed Insights metrics are green except the interaction to NextPaint. This is the Notion website. Again, all these feed metrics are green except interaction to NextPaint. This metric, interaction to NextPaint, is a new performance metric introduced by Google. It measures how fast clicks or keyboard input are on the page. And in almost every React app or website I've seen so far, this metric is yellow or red. And also, this metric becomes a core vital in March 2024, which means that just in a few months, your CTO, your browser, your marketing team will also become suddenly aware that this metric is yellow or red.
Now, who here has worked with this metric before? Has anyone tried optimizing it? I saw one hand. Okay. Well, then let's take a really quick dive into theory. And to talk about theory, we would need to cover three things, the interaction, the interaction to NextPaint, and the interaction to NextPaint for the page. So, here is a React app. It's a simple note taking app. I could take notes. I can't see actually if I'm typing. I can take notes. I can open notes. I can create new notes. And I can filter notes. And the filtering notes interaction is slow. If you look at the spinner that spins in this corner, which spins when the page is idle, which freezes when the page also freezes, you would see that whenever I type into the filter, the spinner freezes for a second. And this is a slow interaction, and this is what makes the interaction to NextPaint bad.
2. Understanding Interaction to NextPaint
I don't know how this interaction affects NextPaint. To understand, I install the Web Vitals extension from the Chrome Web Store. By enabling console logging, I can see the duration of each interaction on the page. The slowest interaction becomes the NextPaint metric, which can cause issues in the future. Google derives this metric by collecting INP values from websites.
Now, I can feel that this interaction is slow, but I don't know how actually this interaction is slow. I don't know, I don't know how actually it affects interaction to NextPaint. I don't know anything about interaction to NextPaint, right?
So, one thing I really like to do to see how fast interactions are and to see how they affect these new metric that Google has introduced is I like to go to the Chrome Web Store, install the Web Vitals extension. Go back to the app with the red paper. I'm running. Open extensions. Oh, wow. I cannot see from here. Oh, yeah. I can see. Hold on. I need to open the Web Vitals set. Let me do it differently. Yeah, I cannot see where the second one is. So, yeah, this is better. So, I need to open the extension settings. I need to enable console logging. Save settings and then open the console.
And at the moment I do that, and the moment I try doing anything in the app, I would see the extension log every interaction I make on the page. Like whenever I click something, I would see how long that click took. Whenever I type something, I would see how long that typing took. And whenever I try to type into my filter input, I would see how long that filtering action took. I would see that every time I type into the filter input, my interaction takes 700, 600 milliseconds. And I could see that as the interaction goes higher and higher, so does the INP interaction to an explained value also does.
So, what interaction to an explained basically is the new performance metric that Google introduced is it's the slowest interaction that happens on the page during a single user visit. So, I'm a user, I go to an app, I do a bunch of interactions in the app, I click stuff, I type stuff. And the slowest key press, the slowest click out of this is going to become the interaction to an explained metric that Google measures. That's going to be yellow or red, and that's going to create a lot of issues for us in just a few months. So, now, this was the interaction to an explained just for a single visit, but what Google shows is the interaction to an explained for the whole page. So, how does Google derive this number? The way it works is if you opted into sharing data with Google, what Google actually does is every time you go to any website and click on the website, Google collects every INP value from that website.
3. Understanding INP and React Rendering
The INP for the page is determined by measuring the slowest 25% of interactions and visits. In React apps, slow typing into the filter is caused by React rendering all components one by one, resulting in a slow input. This slow render affects interaction, user experience, and the MP. To optimize this, you can try wrapping components with React memory, optimizing the component, virtualizing the list, debounce and throttle, and using useTransition in React 18 to make updates non-urgent.
For that visit, it takes all these visits, it sorts them, it picks the slowest 25% of those visits and names the fastest of these slow 25% of visits as the INP for the page.
So, once again, what INP is, you measure every interaction, you pick the slowest 25% of interaction, then you collect all the slow visits, you pick the slowest 25% of all visits, and you get the INP for the page.
And this INP for the page is yellow or red in almost every React app I've seen. So, well, now, let's get back to our app. Our app is slow, and this makes INP also slow. And if I were to debug this issue, I would see that this issue, this slow typing into the filter is caused by React to render.
Here's what's happening in the app. So, when I type into the filter input, what happens in the app is React calls this set filter function which is a useState hook. That changes the state in a bunch of components, and that causes React to render all these components one by one until it's done. And this is what makes my input slow. This re-rendering is a stop the world operation. Nothing can happen until React is done. So, when I type on the keyboard, the page won't update until React has finished processing all the components, right? If it takes two seconds to process all the components, then the page won't update until two seconds later. This is the classic slow React render, and this is what makes interaction to an expanded bed in React apps.
Now, we have a performance problem, right? I type into the filter input, the app freezes for a second. This makes the interaction worse, this makes the user experience worse, this makes the MP worse. Now, if you were my question to you folks, if you were a developer of this app, how would you try to optimize this? How would you try to make this faster? Spoilers. Any other ideas? Debounce? What? Yeah, well, so, yeah, there are a bunch of stuff you could do. You could try wrapping some stuff with React memory, if there are any components that are rendering unnecessarily. You could optimize the component. You could virtualize the list, if that's a big list, and that's a big list. You could debounce and throttle. And one more thing that you could do, which React is introducing, you could also make updates non-urgent by using useTransition. What does this mean? So, with React 17 and below, every update that happens in the app is considered urgent. If you click a button, React has to handle the update immediately. If you type into the text field, React has to render the list of nodes immediately, and that's a stop the world operation. With React 18, however, your updates can now have a priority. Every update you make in the app is still by default urgent, but what React now also supports is non-urgent updates, and non-urgent updates automatically don't block the page, no matter how long they take. Let's see how this works. So, here is the code for my left sidebar in the app.
4. Optimizing React State Updates
I have the useState hook that controls the state of the filter. Every time I type into this filter input, the useState hook updates and that causes React to render the whole list of nodes and do a really slow interaction. React 18 allows us to take some part of the state and tell React that this update is non-urgent, using the useTransition hook. To apply useTransition, I'm going to split the single bit of state called filter into two bits of state: filterInput and filterValue. I need to make them control the UI components I care about and set both states when typing into the filter. By using the useTransition hook, I can update the bit of state that controls the list of nodes non-urgently, while the filter input updates urgently.
There are a few critical bits here. I have the useState hook that controls the state of the filter. I have the filter input, which renders this input. I have the list of nodes, which renders these nodes. And right now, every time I type into this filter input, the useState hook updates and that causes React to render the whole list of nodes and do a really slow interaction.
So, what React 18 now allows us to do, is React 18 allows us to take some part of the state and tell React that, hey, this update is non-urgent. And the way you do that is you do it by using the useTransition hook. So, let's do just that.
So, to apply useTransition here, I'm going to make several changes. Bear with me. The first change I'm going to make is I'm going to take my single bit of state called filter, and I'm going to split it into two bits of state. I'm going to call them filterInput and filterValue. The first one is going to control my input component. The second one is going to control my list of nodes. This one is going to be called setFilterInput and setFilterValue. Now, I need to take these two new bits of state and I need to make them actually control the bits of the UI that I care about. Done. And now I need to set both of these states whenever I actually type into the filter, right? So, I'm going to do this. Perfect.
So far, what this would do is this wouldn't really do anything. I just took one bit of state and I split it into two bits of state and that makes zero sense, right? I'd still type into the app, the app would be slow, and now for some reason I have two bits of state that store the same thing. However, what I can do now with reg.tint is I can take one of these bits of state and I can tell reg.tint that, hey, when you update this bit of state, the bit of state that controls the list of nodes, please update the bit of state non-urgently, because I don't care whether that update completes. What I care is that the filter input updates urgently because that's where the user types and that's where the user should see the input right away. What I don't care is if the list of nodes takes longer to update because the user can wait. And I do that using the use transition hook. So, let's do just that. To use, to tell, to tell the update that the set filter value hook update is non-urgent, I need to do the following. I need to import useTransition. I need to call useTransition inside the component body. I need to retrieve the isPendingValue and startTransition function from the useTransition hook.
5. Optimizing React Rendering with useTransition
To make the filter input more responsive and prevent freezing, I wrapped the setFilterValue setState with startTransition. This allows the page to remain completely responsive while the expensive render is running. It's like magic! React gives back control to the browser after every frame, using a different mechanism than request animation frame or request callback. The useTransition hook works by managing a queue of updates, prioritizing components that don't need urgent updates.
And I need to wrap my setFilterValue setState with startTransition. Oh, yes. Trans-ti-ti-tion. Okay. Oh, that's why it didn't. Okay. That's why you use useLint. So, all right. So, the change that I've just done is the single change that I've just done is I took my setState function and I've wrapped it with startTransition, right? And if I remove that, if I remove that, if I just type into the filter, you would see that the filter is still, typing into the filter is still slow, right? I type into the filter and it freezes the page for a second because Rect has to render all these components and that takes a lot of time.
Now, if I wrap this setFilterState, sorry, setFilterValue with startTransition, what would happen is I would type into the filter input and the page would not freeze anymore. I type into the filter input and the page is completely responsive. You could see I'm typing and the spinner is spinning and I'm typing more in the list of nodes that renders and it actually, if the render is shipped, it renders right away. You could see that there's no delay like with the bouncing or with throttling. It handles the input right away. If the render is cheaper, if the render is expensive, it just runs the render non-urgently. So this is really, really cool and the page stays completely responsive while the expensive render is running. This is almost like magic.
Now, does anyone have any idea of how this works? Close. No. Any other ideas? Actually, no. But, yeah, you're thinking in the right direction. So the way this works is, sorry, wrong slide. The way this works is giving back control. So Rect starts rendering the app and then gives the control back to the browser after every frame. It does not use the request animation frame. It does not use request callback. It uses a slightly different mechanism and let's see how exactly this looks, which mechanism exactly it uses. So under the hood, here's how use transition works. React has a queue of updates. In our case, this queue has components that don't need to be updated urgently.
6. Understanding React 18's Rendering Process
It's notes lists and a bunch of note buttons. React also has a function called perform work until deadline. This function takes the update queue and processes it one by one and in Rect 17, this was pretty much it. You would start processing the queue and you'd keep processing the queue until you are done. And if this takes two seconds, if you need to render 2000 components then this would take two seconds and you would render 2000 components. All this time the page would be blocked.
Now what Rect 18 did is Rect 18 added two critical changes. First, in the while loop, it added a check called should yield to host, which tells Rect whether it should give the control back to the browser. And second, after the loop, Rect now checks whether there are still any pending unprocessed updates and schedules another perform work until deadline function to be executed in the next frame.
If you remember the slide from the past, this is how Rect 17 behaved when I tried typing it to the filter input. I typed it to the field that changed the state in a bunch of components and Rect rendered all components in one single pass, freezing the page for the whole time. Now, with Rect 17, this changes. When I try typing it to the filter field, Rect calls set filter input and then, immediately after, calls set filter value. This causes the state to update in a bunch of components. But now, some of these state updates are marked as urgent, whereas others are marked as non-urgent. What Rect now does is it still renders the urgent updates in the old blocking manner, but then it starts rendering non-urgent updates in a non-blocking way, giving the control back to the browser every five milliseconds. And because the browser gets the control back, it can repaint the page much sooner. The first interaction is going to be faster because we aren't blocked by all the non-urgent components. And subsequent interactions are also going to be faster. So, like, for example, if the user clicks a button again during... If the user clicks some other button during the non-urgent interaction...
7. Concurrent React Render and Suspense for Hydration
The non-urgent render is not going to take longer than five milliseconds, keeping the interaction to nextPaint metric low. React 18 allows concurrent hydration by splitting the page into sections and wrapping each section with suspense. This non-blocking hydration improves performance by rendering the remaining components after the urgent ones are done, giving control back to the browser every five milliseconds. Wrapping the whole page with suspense can cause forced argent hydration, negatively impacting INP.
The non-urgent... Sorry. During the non-urgent render... The non-urgent render is not going to take longer than five milliseconds. So, the onClick event handler and the next paint is also going to happen in no more than five milliseconds, which keeps the interaction to nextPaint metric low. This is the concurrent React render mechanism and this is how it improves interaction to nextPaint.
Now, what React 18 allows you to do is it allows you to take your site and hydrate it concurrently. To do this, you need to take your page, like, for example, this Notion landing page, split that page into sections, like a header, a hero block, more sections, some are further down, and wrap each of these sections with suspense, which would basically look like this in code. You don't even need to specify the full block. Just the suspense tag. And what would happen now would basically be the following. You would call hydrate root, React will start rendering components one by one until at some point it stumbles upon the suspense boundary, which marks non-blocking hydration. And when React stumbles upon this boundary, it won't proceed past that boundary just yet. It would keep rendering the remaining argent components until it's done. And then, only then, once it's done, it will render the non-argent ones, giving the control back to the browser every five milliseconds. Just like with useTransition. And this is what helps inf a lot. Now, you might ask, okay, if this is so good, if this makes the page hydrate non-blockingly, why do I need to bother splitting the page into sections? Why don't I wrap just the whole page with suspense? And the answer is that it would actually flop your INP. Why? This has to do with another suspense behavior from React 18, which I call forced argent hydration. There's no official name, just my name. The challenge behind it is the moment you click some suspense boundary that you have in your app during hydration, React switches that suspense boundary to argent hydration. So if you wrap your whole page with suspense, it will hydrate like this five milliseconds at a time, right? But then, if you click the page right during, right when it's hydrating, what would happen is React will switch to the argent hydration for the remaining part of the page.
8. Understanding the Impact of Wrapping with Suspense
If you wrap separate page sections with suspense, only that suspense boundary will hydrate urgently, making the next paint happen earlier. This is why you don't want to wrap the whole page with suspense. Bad INP numbers on Google can negatively impact SEO and user experience. The number you see in page speed insights reflects how slow it feels for users. Google is adding this metric now because they have designed it correctly, unlike its predecessor, first input delay.
So you would click the page and the page would become frozen. It would basically work as if you didn't have any suspense at all. Which means the next point won't happen until that argent hydration is complete. Whereas if you wrap separate page sections with suspense, which will give you rendering that looks like this, and then click some suspense boundary, then only that suspense boundary will hydrate urgently. And that will be much faster. And that will make the next paint happen much earlier as well. So this is why you don't want to wrap the whole page with suspense.
All right, so this is interaction to next paint. This is the two correct features that help with interaction to next paint. This is how they work. And this is how they help. So take them, take your INP and go make your INP green. Thank you. That's always a good thing.
Maybe let's start with this one. Let's not look at the user side of things. Just like how bad is a bad number on Google for me? Do I actually need to care as a developer? Well, first of all, it matters if you care about SEO, it's going to be really bad for your SEO. If you don't care about SEO, I think there's not a big business incentive. It's still going to be... Sorry, you can believe me later. It's going to suck for your users. And if you care about your users, that's something still to care about. Because the number you see in page speed insights, that's how slow it's actually for your users. It's not just some fake number measured in some fake browser, it's how it actually feels for your users. But apart from that, it's mostly SEO and search ranking.
Okay. Yeah. Thank you for that. And let's focus on why is Google adding this now? Well, so why didn't they add it earlier? Because they didn't know how to design it right. There's a predecessor to this metric called first input delay.
9. Replacing First Input Delay
First input delay is going to be replaced with interaction to the next page. Google's business interests align with making the web faster. They want to motivate people to prioritize web as a good user experience. This metric helps in achieving that goal.
You probably did not notice it because it's green pretty much everywhere. And that's the issue, because if it's green everywhere, it does not show you anything. So first input delay is going to be replaced with interaction to the next page. Why didn't they add it later? Why now? Why not later? Well, because they're trying to make the web faster. I know it's in their business interests. And if it's in their business interests, are there other interests which might align better with other metrics?
Oh, sorry. I can't get the question. No, I'm just wondering. I mean, they're in the business of selling ads. So there's a specific reason to specifically pick that metric, whereas compared to another business, you might care more about other metrics. So my understanding is that basically Google benefits where web is fast, right, for people who choose web or mobile apps. And to make web a good user experience, they need to, well, motivate people to make web a good user experience. And this is one of the metrics that motivates people.
10. Limitations of use transition
Use transition is not effective when dealing with a single component that takes a long time to render. It cannot split the component into chunks, causing the render to complete before giving control back to the browser.
Talking about another metric, are there cases which are bad for the interaction to the next pane, which are not fixable by use transition? Yeah. So one of the — this is actually also another thing that did not fit — one of the things where use transition would not help is when you have a single component that's very expensive. So use transition splits the work and the border of components, right? If you have a lot of cheap components, which is most of the case, that's going to work fine. But if you have a single component that takes 500 milliseconds to render, use transition would not be able to split that component in chunks. It would start rendering the component, and then only after that render completes, it would be able to give the control back to the browser. So that's one big case where it doesn't help.
11. React Performance Optimization
Developers on React 16 can still optimize performance by debouncing, throttling, and virtualization. The focus on performance depends on the stage of the company, with startups prioritizing survival and then performance. Big companies with a product market fit should aim to capture additional users through better performance. The 80-20 rule for performance includes using tools like Next.js, CDNs like Cloudflare, and basic performance monitoring. Implementing these strategies can significantly improve performance.
About cases that are not helping, what can our developers do if they're still on React 16? Yeah, I don't know. But, I mean, the list of optimizations that we briefly touched, it's way from full, right? But there's definitely a lot of stuff you could do on React 16 and 17. It's just harder, and you need to profile more and think more about it. It's debouncing, throttling, I know. Virtualization.
Related to that, how much should we focus on performance while developing stuff, or should we only have a look at it when we notice specific performance issues? Oh, so if a performance consultant, I'm gonna say, no, actually, I don't have a good answer to this, right? My current opinion, which I'm very happy to be disagreed with, my current opinion is if you're in a startup, then your first goal is to survive. Your second goal is performance. Focus on features, focus on product market feeds. Then if you find the product market feed, focus on retaining new users and keeping performance good. If you're in a big company, then, well, if you already have the product market feed, then I guess you're interested in capturing the take-survive 10, 20% of users that's gonna come to you if you have a better performance. So at that point, it does matter much more than the beginning.
And what are your 20, 80 rules for performance? If someone's wants to focus on the users, but has a bit of time budget? Wow, okay, I was not prepared to answer that. But well, I don't know. Take next.js. I know some people are going to complain about next.js later on this stage, but next.js is still good enough. Take some CDN like Cloudflare, put in some basic performance monitoring, like if you're center-centric and do some basic performance monitoring, like do these three things, that's going to be 80-20. And yeah, you could take it from there later. Quick check with the audience, who's already doing all of that? I see three hands. So we can probably improve the performance a lot by just doing that.
12. Favorite Metrics and Nesting Non-Urgent Renders
What are your other favorite metrics? LCP is a frequently mentioned metric in React apps. It is affected by time to first byte and CSS loading. Optimizing LCP can lead to significant improvements. Can non-urgent renders be nested inside non-urgent renders? I can't answer right now, but it's an interesting question to explore further.
Okay. What are like your other favorite metrics which might come up in the future, which we should be aware of? And what about time to next slide? Yeah. Well, my favorite metrics... Okay, well, I don't know. I could tell you that my favorite metrics are like three Google Corovitals. That's not really true. It's Google's favorite metrics. I still just... I have to deal with them all the time. I think in React apps, what comes up really often is also LCP, Large Discountful Paint. It's partly affected by time to first byte, but it's also affected by how you load your CSS, for example. Do you have a waterfall of server requests? Is your server on the same domain or on a separate domain? There's like a bunch of stuff that goes into it. And I really like optimizing that, and I think there's a lot of cool stuff there to talk about.
But while we're talking about optimizing, maybe one last question. Can you actually render the non-urgent renders inside non-urgent renders in some cases? Sorry, say it again? Nests, urgent render site, non, do, do, do, do, do, do. Great question. I can't, yeah, I can't answer right now. Sorry. My mental model is... I need two minutes to think about. I wasn't able to answer this. I mean, I think that's great because we need to close it anyway. But you can follow up on that in the discussion room. Thank you.