Video Summary and Transcription
React's improvements in performance, such as the introduction of useEffect, have gone unnoticed. useEffect simplifies synchronizing logic and improves performance by eliminating forced layout calculations. Update batching optimizes rendering by combining multiple set-state calls into a single render. React 18 introduces batched set-state calls for faster performance. React Suspense and selective hydration improve user experience and debugging performance issues is best done practically. Server components, recommended debugging tools, and framework choices are also discussed.
1. React Performance and useEffect
React's improvements in performance over the last ten years have often gone unnoticed. Today, I want to discuss the hidden changes that have made React apps faster. One such improvement is the introduction of useEffect, a hook that simplifies synchronizing logic. With useEffect, developers no longer need to use lifecycle methods like componentDidMount, which required more code. However, alongside the usability improvement came a deliberate performance enhancement, which I will explain using a quiz.
So, let's get started. Original title of this talk was last ten years of React performance, so to start, I'm pretty curious, folks, who here has already used React 19? Could you raise your hand? Oh, nice. Who here has ever used React 18? Nice. What about React 17? What about 16? What about 0.13? What about 0.3? I do not believe you. That was the first version of React. Well, maybe some people did.
Anyway, React has been around for ten years, or at least ten years it went through an awful lot of changes. Some of them improved compatibility, some of them directly aimed to improve performance. But today, I want to talk about the improvements that most developers didn't notice that I didn't notice for years, improvements that React slipped under our noses to make our apps faster. In other words, I want to talk about the invisible hand of React performance.
To start, let's talk about useEffect. Who here uses useEffect? Who here loves useEffect? One hand! Okay, yes, I'm going to show you one reason why useEffect is actually a pretty good hook. So, useEffect is a hook that lets us synchronise things, right? We can run some logic when the component mounts, or when some props change, or on every change. Before useEffect was introduced, the same thing was achievable with life-cycle methods. You would have a class component, a component written as a class, not as a function, and that class would have a render method that returns the JSX and methods like companyDidMount or companyDidUpdate which would allow me to synchronise prop changes just like useEffect but with more lines of code.
Now, when React 16.8 came out, useEffect pretty much replaced companyDidMount because it was just so much easier to use, and that was a great change, but alongside the big noticeable usability change came a much more hidden but very intentional performance change which I would like to talk with you about. And to talk about it, I have a quiz. So, I have this React component. When the component renders, it sets the document body background colour to red. When the component mounts, it sets the background colour to blue. Question, which colours will the browser render? Option A, red, then blue. Option B, blue, then red. Option C, only blue. Option D, only red. Which colours will the browser render? Who thinks it is A? Okay. Who thinks it is B? All right. Who thinks it is C? Nice. Who thinks it is D? All right. So the right answer here is C, only blue. Now, I have this code.
2. Effect of useEffect on Browser Rendering
With useEffect, the browser renders the colors red, then blue, while with companyDidMount it only renders blue. This difference in behavior is a deliberate performance decision made during the design of useEffect, which fixed a class of performance issues. To understand this difference and the issues it addresses, we need to understand how browsers render updates and utilize caching. When reading a value that is not cached, the browser must recalculate styles and layout, resulting in a performance issue known as forced layout calculation. This issue was commonly encountered with component-mounting.
This is the same code, same component but instead of companyDidMount, it uses useEffect. Same question. Which colours will the browser render? Who thinks it is A, red, then blue? Who thinks it is B, blue, then red? Who thinks it is C, only blue? Who thinks it is D, only red? All right. Quite a few people. The right answer here is A, red, then blue. So, with useEffect, the right answer is red, then blue. With companyDidMount, the right answer is only blue. This difference is a result of a very intentional performance decision that drove useEffect design and also helped to fix a whole class of performance issues. But to understand this difference, to understand the issues it fixed, we need to take a detour into how browsers render things under the hood.
So the browser's rendering pipeline is complex. You only need to know two things about it. Thing number one, whenever a browser has to render an update, like maybe it's a timer fired, maybe I clicked something, whatever, it always goes through four stages. First, it runs the JavaScript. Then, if the JavaScript has updated the page, the browser recalculates the styles and recalculates the layout. And then, once it is done, the browser paints the results on the screen. This is the first thing we need to know, always four stages in any update. The second thing is that browsers use a lot of caching. So every time the layout updates, for example, the browser caches it and remembers it until the next time the layout has to update again. So for example, if you render the app for the first time in this update N minus one, and the browser calculates that the header has a height of 200 pixels. The browser will remember that, and keep that in memory until you say click the header, and it expands, and the header becomes 500 milliseconds, because it has changed. So when you have, for example, a button ref in React, and some JavaScript code tries to read the height of the button, the browser doesn't need to do that right on the spot. The browser will just go to the cache and read the value from there when it's been saved after the last update.
Now, imagine what happens if the button which height we are reading was just mounted? What happens if it's not yet in the cache? Take a second to think how the browser would behave then. So what would happen then is the browser will be forced to recalculate the styles and recalculate the layout ahead of time. When I'm reading something that's not in the cache, the browser is forced to recalculate the styles and layout ahead of time. So when that happens, the browser will freeze the JavaScript, compute the styles, compute the layout, and then return the height back to JavaScript. This is a performance issue that's caused a forced layout calculation. This was a pretty common problem with component-mount. Say you have a component that looks like this. Say it's a button component, it renders a button, and then it checks if the button is taller than 300 pixels, and, if yes, then it does something with the button.
3. Batching and Performance Optimization
Company-did-mount requires the browser to perform a forced layout calculation, causing delays. In contrast, useEffect runs in the next frame, allowing the browser to compute and cache the layout before querying it. This eliminates forced recalculations and improves performance. The introduction of useEffect was primarily driven by a simpler API, but it inadvertently resolved a class of performance issues. React's invisible optimizations make our apps faster without us even realizing it.
It uses company-did-mount. In React, it runs right after render within the same block of JavaScript. So it needs to read from the layout, like in this case, and this layout isn't cached which is very common because React renders the update the page, right? They often invalidate the layout. What would happen is the browser will have to do a forced layout calculation, which will freeze component-did-mount until it completes. This is a common issue, and this is a common enough issue that you would see even DevTools worrying about it. If you ever did the performance recordings of kind, you might have seen these violet highlights. This is the forced layout recalculation. This is what happens when you read from the layout where the caches aren't there yet.
Compare this to the component which uses use-effect. Unlike with company-did-mount, use-effect runs not in the same frame but in the beginning of the next frame. This changes everything. Now, if I render a button, first React would run the JavaScript, then the browser would compute the styles and the layout, then it would paint the results on the screen, and then, only once all this is over, use-effect would run and try to read from the layout. Now, when use-effect reads from the layout, the layout will be already computed and cached because it's been done as part of the previous update, it's been done right here, so no forced layout would happen. You simply read from the cache. If you check DevTools, you wouldn't see any forced recalculations anymore. You would only see JavaScript, style, layout, paint, and then in the next frame, a little more JavaScript.
Now, notice how none of the reasons why use-effect was introduced were about performance. When React 16.8 came out, the reason to migrate to use-effect was because it provided a simpler, easier-to-use API, but when we moved to this new API, this issue, this whole class of issues, they basically disappeared. This is the example of the invisible hand of React performance. This is how React made our apps faster without us even realizing that. Oops! Spoilers! Let's talk about the next thing, batching, and, for that, I have another quiz. So, here's the button. This button has an on click. The on click calls this make document public event listener, and the event handler has four set state calls. Set state one, set state two, then some API calls, set state three, set state four. Question. How many renders will this component have? Option A, three. Option B, four in React 17, three in React 18. Option C, three in React 17, two in React 18. Option D, four in React 17, two in React 18.
4. Update Batching
Update batching is an early React performance optimization introduced in version 0.4. It underwent significant changes and received the most significant overall improvement in React 16. Batching allows multiple set-state calls to be combined into a single render. In React 18, set-state calls one and two, and three and four are batched, resulting in two renders. In React 17, only set-state calls one and two are batched. React's implementation of batching evolved over the years, starting with a simple update process and later introducing more complexity.
Who thinks it is A? No hands. Who thinks it is B? Two and a half hands. Who thinks it is C? Quite a few hands. Who thinks it is D? Most hands. All right. The answer here is C. This is happening, thanks to update batching, which is one of the earliest invisible React performance optimisations. It was introduced all the way back in version 0.4 in 2013. It changed quite a bit over the next ten years, and it received the biggest overall in React 16.
So to see why exactly it is four in React 17, sorry, three in React 17! To see why it is three in React 17, two in React 18, let's talk about how batching works. Batching is when you have several set-state calls but they only result in a single render. React 18 has two renders because set-state calls one and two, and three and four are batched together. But in React 17, only set-state calls one and two are batched. Three and four aren't. Raise your hand if you know why. I see one, one, one, two hands. Great! For everyone else, let's talk why. To understand why, let's see how React implements batching.
Say I was meta, or Facebook back then in 2013, and I was implementing React from scratch. What would be the simplest way I can implement a set-state call? It would be something like this, right? Whenever someone calls set state, I take that state and perform the update right away. That's also how React was working in the very first releases before batching was introduced with different function names of course. React 0.4 introduced batching. Subsequent versions fixed some edge cases. In the recent versions of React up until React 18, set state actually started looking a little more complicated.
5. React Batching and Update Processing
In React 17, set state updates are queued and processed based on the batch updates flag. When the flag is true, updates are processed in the event listener. This results in only the first two set state calls being batched. In React 18, all updates are batched by default, and the process update queue function is executed at the end of the current frame. This ensures that process update queue is only called once, even if set state is called multiple times.
Here's how it looked in React 17. First, in React 17, whenever you would call set state, React wouldn't process the update right away. Instead, it would put the update into the queue. Second, after queuing the update, React would check if it's batching the updates right now. If it's not, if the batch updates variable is set to false, then set state will process the update right away. But if it is, then no processing will happen. Set state will only push the update into the queue and then do nothing else. Batch updates, by the way, is simply a global flag that various parts of React can set to true or false. If you're batching, if some part of React has set the batch updates flag to true, and set state isn't processing the update, isn't calling process update queue, then where would the update be processed instead? The answer is the update will be processed in the place that sets this batch updates flag to true, specifically, in the event listener. Here's how this works. So when you have a button with a non-click, and you pass a callback into that non-click, and that click fires, the following happens. First, React sets the batch updates flag to true. Second, the callback fires. If the callback makes any set state calls, these are pushed directly into the queue, and not processed because the batch updates flag is true. Third, the flag is set back to false, and, fourth, now that we're done handling the click, any updates, finally, that were scheduled are processed. This is how batching works in React 17, 16, 15, and that's why, when you have this code in React 17, only the first two set state calls are batched. Because the moment you await your original callback, this callback completes, and batch updates switches back to false. Now, how does React 18 await this issue? How does React 18 batch updates happen after a wait? The trick looks as follows. First, in React 18, the global batch updates flag is no more. All updates are now batched by default. The event listener wrapper, now that there's no flag, simply calls the callback directly. Second, the process update queue function is still called but in a very specific manner. Whenever you call set state, React executes promise resolve then process update queue. What this achieves is this runs process update queue but not immediately, but rather at the end of the current frame. Finally, React ensures that it only calls process update queue once. If you call set state more than once, then process update queue will still only be scheduled once. So, here's what this does. You should remember this chart from a little earlier. This is how it works. You have a frame.
6. Update Batching in React 18
In React 18, set states three and four are now batched, resulting in faster performance. The updates are processed together after the JavaScript completes, utilizing update batching.
The frame starts running on click, then on click calls set state three times. Every set state pushes into the queue, and then, well, only the first state schedules the process update queue function. Then on click completes, and maybe some other JavaScript runs if there's anything else to run, and only after all that, only after on click and all other JavaScript has finished, the scheduled process update queue function finally runs, and processes the updates, all the updates, all the queue that was collected during set state calls. This is how in React 18, set states three and four are now batched. You call set state three, it pushes the update into the queue and schedules processing but does not process anything right away. You call set state four, it pushes the update into the queue and again doesn't, well, this time doesn't schedule any processing because it's already been scheduled, and then JavaScript completes, and process update queue kicks in, and then both updates are processed together. This is update batching, and this is another invisible hand of React that makes our apps faster out of the box.
React Suspense and Hydration
Let's talk about suspense in React. During hydration, if a button is clicked, the page freezes. However, when the header is wrapped with suspense, the click is ignored.
All right. Let's talk about one more thing, which is suspense, and guess what? I have another quiz.
So here, I have a little bit of JSX. It's a header component with a few children. There's an icon, a menu that's collapsed by default. I don't know why this laser pointer is so laggy. And a button that opens the menu.
Now, imagine this is a server rendered app. If it's a server rendered app, there will be a few moments when the app won't work as expected, right? For example, if you just open the page and no JavaScript has loaded yet, the bundle is still loading, you can click the button, but the button won't do anything, right? Because there's no JavaScript yet. And if the JavaScript has been loaded, but the app is still being hydrated, well, this is my question to you.
What happens if the button is clicked during hydration? Option A, nothing. The click is ignored. Option B, nothing, a warning is logged. Option C, the click is remembered and is replayed once hydration completes. Option D, the page freezes. So who thinks it's A? Quite a few hands. Who thinks it's B? More hands. Who thinks it's C? Even more hands. Who thinks it's D? Two hands. The right answer is D. The page freezes. Hydration, by default, freezes the page for as long as it takes to hydrate the app. If the app has 10,000 components, for example, the page will be frozen until React renders all 10,000 components. If you click the page during hydration, the page will be frozen.
All right, another quiz. Here's the same app, same question, but this time, the insides of the header are wrapped with suspense. Same question. What will happen to the page now? Option A, nothing, the click is ignored. Who thinks it's A? No. One hand, two hands.
React Suspense: Selective Hydration
Option B - nothing, a warning is logged. Option C - the click is remembered and is replayed once hydration completes. Option D - the page freezes. Here's how suspense works: wrap parts of your page with suspense for data fetching and introduce selective hydration.
Option B, nothing, a warning is logged. Who thinks it's B? Option C, the click is remembered and is replayed once hydration completes. Who thinks it's C? Quite a few hands. Option D, the page freezes. Who thinks it's D? Two hands. The answer is still D. The page will freeze again, but this time, it will freeze for a much shorter period of time, only to hydrate the elements inside the suspense.
Here's how this works. Suspense generally is a component that's used for data fetching, or maybe not anymore if I were to believe the latest angry X threads. The idea is that you wrap some parts of your page with suspense, and then, when you're fetching something from inside this part, suspense will render the fallback. However, that's only one part of the idea behind suspense. A much less noticeable part of the idea is that suspense also introduces something called selective hydration.
The idea is described in a discussion in the Rack 18 working group, and I also go in depth into it in my other talk, but the gist is as follows. As your site or app grows, more and more parts of it are going to get wrapped with suspense to do data fetch, like maybe if you're the Airbnb website, you will have the search wrapped with suspense, and maybe you will also have each listing wrapped with suspense, and so on.
Rack Behavior and Selective Hydration
When the page hydrates without suspense, Rack freezes the whole page. But with suspense, Rack's behavior changes to hydrate suspense by suspense. Use layout effect has the same behavior as componentDidMount and componentDidUpdate.
What Rack does without suspense is when the page hydrates, Rack freezes the whole page. It starts hydrating the logo, then it starts hydrating the header, then it starts hydrating the search, and then the listings, and then so on and on and on until the whole page is hydrated. If you click something, you will have to wait until the whole hydration is complete.
But with suspense, Rack's behaviour changes. Rack will now hydrate the app suspense by suspense. It will hydrate one suspense, make it interactive, switch to the next suspense, also hydrate it, switch to the next suspense, also hydrate it, and I'm supposed to be showing the suspenses here. Sorry! This suspense, this suspense, you get it. If you click any of the suspenses while the page is being hydrated, the page will freeze until only until that suspense finishes hydrating, which will be much shorter. This is selective hydration.
Use layout effect has the same behaviour as componentDidMount, componentDidUpdate, and this is the reason React discourages you from using it, because if you use it, you have a risk of reintroducing the same performance issues that componentDidMount had.
React's Invisible Hand and Selective Hydration
React ships invisible performance improvements. Examples: useEffect, batching, suspense. More examples not covered. React gets criticism, but I appreciate this invisible hand. Q&A: useLayoutEffect same as did mount did update. Cost of adding suspenses? No significant drawbacks. Selective hydration: Rack hydrates suspense boundaries top to bottom, events like click force hydration.
That's a whole separate talk again. But this is yet another example of React shipping invisible performance improvements under our noses without anyone noticing. So, use effect, batching, suspense, all those are examples of React's invisible hand that made our apps faster. There are quite a few more examples that I wanted to talk about, some successful, some not. Sadly, they didn't fit in. I know React gets quite a lot of burn, sometimes deserved, but this invisible hand is honestly something I really, really like.
Thank you! Let's get into the Q&A. You can still ask questions with the code 0614. I'm excited to see what people are asking. The suspense is killing me!
All right. Does use layout effect have the same situation as did mount did update? Yes. Use layout effect has the same behaviour as company did mount, company did update, and this is the reason React discourages you from using it, because, if you use it, you have a risk of reintroducing the same performance issues that company did mount had. Guys, shhh in the back, thank you! Thank you.
What is the cost of adding suspences? What if I add 100 of them on a page? I asked the same question. I asked Dan Abramov about this, like, hey, what happens, like, why can't I go and wrap every single element on the page with suspense? Is there a new drawback to this? The answer that I received was that there's basically no significant drawbacks. The only issue that you will have is that if anything suspends, actually suspends because it is fetching something, then, like, you will see the fallback rendered. If you forget to provide a fallback somewhere, that's going to be, well, the element would flash, the element would disappear. But apart from that, I'm not really aware of major drawbacks. Thank you.
There's a couple of questions around selective hydration. Can I choose what to hydrate first, like in the Airbnb example, and does it only happen on click, and can you tell us more about that? Great question. So the first, can you select what to hydrate first? As far as I know, Rack just hydrates all the suspense boundaries from top to bottom. There's probably some, like, data fetching or, like, waiting for server data part involved, but there's no API I'm aware of that would let you tell, like, hydrate this first. So, not only on click forces it, what forces hydration is all the events that Rack calls discrete which are basically on click, on input, and I think that's it. So stuff like mouse moves or scrolling that shouldn't force hydration. Thank you.
You seem to be very in the weeds of the React perf ins and outs. Someone asks... Not by choice.
Debugging Performance and React 19 Features
Not by choice. Would you recommend going through the React source code? No. It gets outdated quickly. I prefer a practical approach to debugging performance issues. I focus on the relevant parts of the source code. Ivan forgot the performance features of React 19. Static components and server components are his current favorites.
Not by choice. Not by choice. Would you recommend going through the React source code? No. I have never done that. It's probably... Well, I know some people who have done that. They made very great blog posts, which I've read. This blog post gets outdated within several months. So that's one reason why not. The second reason why not is, at least for me, it works in a more practical way. When I have some performance issue that I need to debug, then I go and try to figure out what makes the performance issue slow and then I don't read through the whole sources, don't go through the whole sources, but I go through the part of them that makes sense to my context and then I understand it much better. Yeah. That's... Look, he's doing the hard work for you, really. What? You're doing all the hard work for them. Let's come ask you. Well, before I was a consultant, so... There you go. Before I would do that, yeah. Not anymore. Ivan, what performance feature of React 19 are you most excited about? Oh, shoot. Hold on, I forgot the performance features for React 19. I do not remember the performance features for React 19. What about your current favorite? All of the ones on the list. Oh, right, sorry. Hold on. Static components gets stable in React 19, right? Then it's then. Then it's server components. Okay. Yeah.
Server Components, Debugging, and Framework Choice
A lot of love for server components. Ivan explains how suspense affects hydration. Recommended tools for debugging React web apps: Chrome DevTools, React Profiler, and Why Did You Render. Astro and Svelte are considered performant frameworks. Ivan cannot answer when to use many requests with Suspense.
I found it. A lot of love for server components in this room, am I right? Server components, yay! Wow! One person. Wow, wow, wow, wow. Let's see. So many questions. Can I see a show of hands, who is this person super interested in PDFs? Come find me later, we'll talk.
Ivan, does clicking suspense push it to the top of the hydration queue, and does it make this one hydrate faster even if, for example, it was scheduled to be hydrated later? So it pushes it to the top of the hydrate queue, clicking suspense, but I don't think it makes it hydrate faster because there's nothing really to make faster. It needs to render all the components inside the suspense boundary. There's nothing really you can sacrifice. Fair enough.
Djer asks, what tool would you most recommend for debugging slow, big, and dynamic React web apps? So React web apps. Well, I do this all the time, and I normally use Chrome DevTools and React Profiler. Yeah. I don't really know, but there's also Why Did You Render, which is a library, not a dev tool. It's also very helpful. Sorry, it's called Why Did You Render? Why Did You Render, yeah. If you Google for it, you'll find it right away. Is there anything else I use? Not really. No, it's mostly that. And in your experience, is there one or other of React framers that you consider to be most performant out of the box? Ooh. Ooh. Well, Astro? There was just a talk about Astro. Yeah. Depends if it's app or content, I guess. I haven't worked much with other frameworks, but back when I was a consultant, my clients worked with other frameworks, and I think I saw the fastest apps with Astro and Svelte. With Astro you can use React, with Svelte you cannot use React. But that's what I saw. This is more of an advice type of question. When should I make many requests with Suspense instead of a big one with all the data and vice versa? Sorry, can I? When should I make many requests with Suspense instead of a big one with all the data and vice versa? I am not the right person to answer that. Sorry.
Performance Factors and Recommended Resources
Ivan discusses the factors affecting app performance and the use of Suspense. He recommends resources such as Twitter, perf.email, and the app formerly known as Twitter.
I can tell you if it's slow, I can tell you why it's slow. This mostly depends... I don't know. Structure it the way you want. Yeah, sorry, not the right person.
And slightly related, will Suspense slow down our app? It depends, I guess. It would probably make it faster. It would change nothing. It shouldn't really slow down anything. At least number-wise. UX-wise, if you misuse it, maybe you would have a ton of spinners loading on the page, and UX-wise that would make the page look worse. But that's a more detailed discussion. Yeah, that's right. Sorry, it depends. You can come find Ivan in the Q&A booths later if you want to go really in-depth on that it depends.
One sort of last one from me because we are running out of time. Do you have any resources that you recommend to people to either learn more about React performance or learn about how to best use it in their apps, make the most of it? So a bit of self-promotion, there's my Twitter, or ex. Twitter. At IAMAkulov, my last name. There's also a really good performance newsletter called perf.email. It's not about React specifically, but it's the best collection of performance resources, regular performance updates that I found. React specific, I don't know, I think I learned most of the stuff from my ex. Not the ex, the person, the app. It's always confusing. Formerly known as Twitter, that one. Formerly known as Twitter, yes.
And this is not a question, but somebody says superb talk presentation, by the way, thanks, and I agree. Thank you so much for your time, Ivan. Please let's give it up again. Thank you for joining us. Thank you. Thank you. Thank you.
Comments