Lesson Description
The "Anatomy of a Re-Render" Lesson is part of the full, React Performance, v2 course featured in this preview video. Here's what you'd learn in this lesson:
Steve explains how React handles rerenders and explains the three main reasons why components may rerender: when a component's state changes, when the context changes, or when its parent changes. He also discusses strategies to prevent unnecessary rerenders, such as memoization and optimizing the use of the context API.
Transcript from the "Anatomy of a Re-Render" Lesson
[00:00:00]
>> Steve Kinney: We're going to talk a little bit about how React does what it does. Why does anything re-render in an application, right? Usually it's one of these three reasons, but this is actually a pretty comprehensive list, right, because without any memoization, this is a lot of times your entire application, right? Given components state changed, right? So either useState or if you still have a class component in your life, set state, so on and so forth.
[00:00:33]
The context changed, so the context API which will definitely, just out of the box, if you read the half blog posts that everyone's written, will cause re-renders in shockingly interesting ways all throughout your application. And we'll talk about some high level strategies for dealing with that, or its parent changed, right? And that's kind of the first one that we will deal with, because in the baby's first React app, usually a lot of the hooks and state management live in that application component.
[00:01:09]
And so those are out of the box, the three reasons. And that is true even like it will keep checking the parent unless we kind of go in there either by hand, or as we'll talk about a little bit later, the React compiler, to say like, no, no, no, no, we can do, we're good here. We can stop going down this rabbit hole together. If this hasn't changed, we can assume that none of his children has changed.
[00:01:32]
Now, if there's a context API somewhere down there, guess what? We're going again, but generally speaking, there are some patterns to stop at least the entire tree from re-rendering, right? Some of them now can be automatic, depending on if you can support React compiler, depending on what version of React, so on and so forth, and then some of the time because that's only when the heuristic from the compiler works.
[00:01:58]
When that happens, when it's like, let's figure out what changed, something changed in the state of our application and React, and I don't think this is a pun intended or unintended, when we react, reacts to the change of the state, right? It goes through a series of steps, and we will break all of these boxes out a little bit and talk about what all those steps entail. But first, there is this idea of a render phase, which we will discuss again in a moment.
[00:02:26]
The render phase is like, hey, let me see, let me call all those functions, right? For the purposes of this workshop, I'm going to forget that class components ever existed, just because it makes the mental model easier. It all applies in both cases, but the metaphors are way easier when I have to use one. Right. And so the whole initial premise of React is you've got this virtual DOM, and so we can kind of do it all in memory.
[00:02:52]
Doing stuff in memory is faster than actually mutating the DOM and causing repaints and relayouts and reflows of the page. Let's go, let's call each function. If you think about it, if a component is just a function, we call that function, we see if it changed. What its act of rendering as children is calling those functions. We move down the entire data structure in memory, we collect all the changes, we get to the commit phase, which is like, all right, let's go do it to the DOM now.
[00:03:19]
And React has a bunch of heuristics which are like, hey, we know that for this type of change, like this surgical moving around of two DOM nodes is actually more performant. Sometimes it's counterintuitive, but blowing away the entire section of the DOM and putting all the DOM nodes back in there from scratch is actually faster than a series of surgical maneuvers, right? And you don't think about that because the React team has thought about that, right?
[00:03:50]
And yes, we've all seen the cases where a team of super duper senior engineers can build a vanilla JavaScript app that is faster than a React app because you can fine tune it to perfection, but generally speaking, the heuristics are good enough for larger teams where you've got a wider range of skill sets, deadlines where perfection is not an option. It's a good enough algorithm based on some real world experiences, right?
[00:04:18]
And so we'll go ahead and make those changes and then do a few other cleanup things like call useEffect and a few other things towards the end, and then the application should be in a settled state until something happens again, and we start the entire process all over, right? And so we'll dig into each of those a little bit. Again, some state change happens and it just moves through the DOM calling every child as we go through until we've gotten to the bottom of the tree, and then at that point we can kind of figure out what those changes are.
[00:04:52]
Now, one of the things we're going to talk about today is either A, using memoization to stop it on its way down, to be like, if we're good at that middle layer, we don't need to keep going. But that could be, that's nuance, right? Maybe. Right? And so sometimes we have previously needed to opt in or tell React. The other move is just have the change start at a lower portion of the tree. Either not going down the entire tree is one, doing less stuff, or B, starting at a lower level on the tree and ignoring the stuff above it is also doing less stuff, right?
[00:05:33]
And the new nuance that we get in React 18 and 19 is then the ability to decide what is important to be done now and what can wait, right? Previously, before React Fiber, which we will talk about in depth, it was literally calling functions and going through the entire tree was a blocking, single threaded thing, and if something more important happened, tough. Right? Now, there are a whole bunch of primitives and abilities to be a little bit more declarative about what needs to happen when, and those are really interesting and useful things.
[00:06:11]
So it adds a new layer between just starting lower or stopping higher. We now also have figuring out does it go in the fast lane or not. And so we will kind of one, conceptually understand that stuff, and two, do it because that's useful. And like I said, if, as soon as we introduce memoization, we kind of swap out the third reason why a re-render would happen for a more nuanced one. With no memoization, React.memo or what have you, it will go check the entire tree.
[00:06:45]
Despite how you think it works, it will go down the entire tree. If we have some memoization, we can say like, hey, if this component is getting the same props it got last time, because it's a pure function, that means it theoretically gets props and has no useState in it, right? If it got the same props, same thing go in, same component, same DOM JSX should come out, we can tell React, just check to see if it's the same.
[00:07:12]
If it's the same, don't go render to find out if the result was the same because we know if inputs are the same, output is the same. So don't do all the work of rendering it and check the output, just check the inputs. At that point, we would stop it from going down the tree. With the context API with regards to performance and just general mental sanity, is it a good thing or a bad thing? That is so tricky.
[00:07:36]
Right, we'll talk about it more, but I'm going to answer the question now too, because I think about this all the time. So, context API, just so we level set as a group, is this ability to kind of have some amount of, I think global state is not global state, let's call it global state for a minute, right? Outside of that tree and being able to kind of grab something that you need much lower on the tree.
[00:08:04]
Without it, you do this thing called prop drilling, which is, let's say two cousins need the same thing. Right, the highest on the tree or lowest common denominator doesn't feel right in the tree metaphor, but it's not a terrible way to describe it. Like that state has to live, and then has to get passed through every component that doesn't care about it on the way down to the two great-grandchildren, cousins, second removed, that both care about it.
[00:08:31]
And that means that also then if you wanted to move something that was in the sidebar to the footer, you then have to unthread, and this is not a performance issue, this is literally your sanity issue, unthread that through the entire application, thread those props back down, and that's just how people lived. Right? There was a, before the real context API there was the unofficial context API which was internal only, that Redux and a few other things used, which felt like magic at the time, and then we all got access to the magic.
[00:09:05]
So that's good for your sanity and maintainability and also, if you're passing that thing down through every layer, right, especially with the state that arguably changed, it means you've opted out of any caching and memoization the whole way down. Now, there are some primitives to make that better now. There's some strategies for making where you can kind of have half of your cake and half eat it too, that we will talk about in this workshop.
[00:09:28]
But the argument is that now you don't need to do that. You can just kind of hook into this ephemeral state that exists. It's kind of like the force, right? Where it's in between all things and stuff like that. It doesn't have to get passed down to the tree. But that means that if you have too much stuff in that context, a lot of us have a bad habit of making one context provider, shoving everything in it.
[00:09:58]
And like I said before, the second bullet part there is if the context changes, any React components hooked into that context will then re-render, and then theoretically, all of its children. So, one global context to rule all context that has everything in your application is going to trigger probably everything re-rendering all the time, right? So I will ruin some of the surprise for you now. The way to solve that is to split it up into multiple contexts.
[00:10:29]
Now, that then means you have that stack, like that pyramid of doom in your index.ts with all the different context providers. But one of the more powerful things, and we'll see this in practice, is called foreshadowing, is the state changes. Right. And usually if the state that a component relies on change, you probably wanted to re-render it. Re-renders are, they're not bad. If the state changed, and they should re-render.
[00:10:54]
They're only bad if they shouldn't have re-rendered, right? If the thing re-renders, it came out with the same output anyway, you did something that didn't need to be done. If the number of to-dos on the to-do list did in fact change, you should probably reflect that in the UI. The kind of trick that we'll see later on in this workshop is there are things that you get a lot of times in the context of API for what is the state, but then there's a lot of stuff like they clicked the delete button.
[00:11:23]
Right? That shouldn't trigger, like that function shouldn't trigger a re-render. Now, if you've bundled it up with the state that has changed and React cannot tell because now it's a totally new object, well then guess what? React doesn't know and it's going to re-render, right? It's going to re-render, it's always going to re-render versus getting something wrong, right? Even with React compiler, if it cannot figure out what is right, it's just going to stand back and be like, I'm not even optimizing this, right?
[00:11:58]
Because memoization and any of these optimizations done poorly are worse because now you have state that's not valid. Yeah, you saved a render to show wrong stuff, not great. So we balance those things as well. So the context API can be a good thing. Like for re-renders is probably a bad thing, but for lots of other reasons, like, because again, oh, it re-rendered, but the app is still so fast. Okay, who cares?
[00:12:24]
Right. It's, yeah, it's triggering these re-renders, but the whole thing, the entire app re-renders in a millisecond. Don't optimize anything at that point. Why would you? Why would you get rid of all of the ergonomics for gains that don't matter just so you can show off to your friends. Nobody likes that. Can you take the context API and make it performant? Yes, it's clearly been done. Right, with all of the primitives that we will talk about today, right?
[00:12:52]
And how that looks in your application is always going to be unique to your application. So we'll kind of look at it in these kind of hyper-focused ways and then talk about how they are generally applicable. The one other thing I will say kind of as a corollary to the rules is there are really two types of state changes, necessary and unnecessary. We are not trying to prevent necessary renders because that is the point of rendering the component at all, right?
[00:13:30]
Otherwise it could have just been an HTML page. What we're trying to do is either A, limit the unnecessary ones, or B, at least make sure that the higher priority ones get priority, right? And so, yes, there's necessary and unnecessary, but even amongst the necessary changes, some of them are more important. And I will spoil a little bit more of our time together now, which is, if, let's say I'm typing into a search field and it takes a while to reprocess those results.
[00:13:59]
Maybe we're sorting 10,000 records. Maybe you should've been doing that on a server, but we all know we don't always get the APIs we want, right? And we have constraints that we can't do all of the things we read about in a blog post for reasons, right? Maybe it takes a while to sort that list or to filter that list or recalculate all the data in your very pretty graph. That's fine. There's like, sometimes there's not a lot you can do about that, like other than we're going to rewrite everything in WebAssembly, which your boss is never going to approve.
[00:14:33]
Then, you know, what would be nice is if as I was typing in that input field, I could see the keystrokes as I was typing versus the entire thing locking up, right? And so what we'll see is even if you have a long running expensive task, if user clicks and typing still feels snappy and responsive while you show a loading indicator, most people are okay with that, right? Where people get angry, it's kind of like you'd be happier with no Wi-Fi than bad Wi-Fi, right?
[00:15:05]
A lot of times we just need things to feel fast. If it feels like I'm typing, it's responding, we're, especially with APIs, we've become accustomed to waiting a second for things to load. But if I click and nothing happens, or I scroll, and it's frozen, or if I type and I don't see my keystrokes in the input field, I'm like, I hate you. Right. And honestly, you deserve it. So figuring out how to put the important stuff ahead of the stuff we're willing to wait for is where some of the nuance of the new React APIs come in.
[00:15:42]
Cool. So we're going to say that, I'll go back a slide. There's really two types of state changes, necessary and unnecessary, and we can split necessary into two more groups, urgent and non-urgent. So really our priority is do the urgent stuff first, do the necessary stuff second, and to the best of our ability, where it makes sense, right, don't do the unnecessary stuff, right? Unless the complexity cost of trying to go out of your way not to do the unnecessary stuff is worse.
[00:16:10]
And then just let it happen. Like, if it's super fast and putting seven layers of caching is actually making it slower, or adding, there's a cost, not just in performance, there's a cost in complexity to every single line of code in your codebase, and every single dependency you take on. And so you're like, well, Steve said to do this thing all the time, no matter what, he did not say that. He did not say that.
[00:16:33]
He said, it depends is the answer to almost all of the questions. Unfortunately. We'll talk about how they work and why you would consider them because the default is to not do anything, right? And we're trying to get somewhere between do nothing and completely make a mess of your codebase to find where the happy medium for the set of problems you're solving is. So we'll talk about the themes one more time and how they relate to the various concepts that we are going to talk about one by one.
[00:17:06]
You know, not doing stuff is faster. So if we can put our state in the right place so it's not triggering stuff in parts of the tree that don't care, that would be great, and that's just really about the component structure and state management of our application. Seeing if we can skip doing stuff is less work than doing stuff. Again, if it turns out that checking is more work than doing, don't check because not doing stuff is faster.
[00:17:38]
If we can put off doing stuff, that would be great, right? If you've ever built that like, I'm going to have three pieces of state, a loading boolean, an error boolean, and the actual state, and I'm going to juggle all of those things and hope I get it right, letting Suspense tell the React reconciler, we're suspended right now, waiting for a promise to resolve. Check back with us later. Is way easier and more reliable than doing yourself, and also given the underlying React architecture, way more performant, while also being less work for you.
[00:00:00]
I love those things. And then finally, we're loading as much as we need and as little as we can get away with, is the kind of final goal, which is a very nuanced thing for everyone's particular app, so that one gets a little hand wavy, but the principles apply, the execution of those things is always a little wild.
Learn Straight from the Experts Who Shape the Modern Web
- 250+In-depth Courses
- Industry Leading Experts
- 24Learning Paths
- Live Interactive Workshops