We've heard a lot about React's Fiber Architecture, but it feels like few of us understand it in depth (or have the time to). In this talk, Tejas will go over his best attempt at understanding Fiber (reviewed by other experts), and present it in an 'explain-like-I'm-five years old' way.
Understanding React’s Fiber Architecture
AI Generated Video Summary
This Talk explores React's internal jargon, specifically fiber, which is an internal unit of work for rendering and committing. Fibers facilitate efficient updates to elements and play a crucial role in the reconciliation process. The work loop, complete work, and commit phase are essential steps in the rendering process. Understanding React's internals can help with optimizing code and pull request reviews. React 18 introduces the work loop sync and async functions for concurrent features and prioritization. Fiber brings benefits like async rendering and the ability to discard work-in-progress trees, improving user experience.
1. Understanding React's Internal Jargon
Welcome back after lunch. This talk is about React's internal jargon, specifically fiber. Fiber is an internal thing that React uses as a single unit of work for rendering and committing. It's not exposed to users, unlike React elements. Elements describe desired UI state, while fibers are internal and ephemeral. They work together to build great apps.
Welcome back after lunch. I hope you're ready for some deepness in this very advanced conference. Let me just start by saying this is a talk that nobody needs to hear. Like, for real, like nobody needs to hear this. I hope some of you want to hear this. Do you want to? Okay. But nobody, because here's the thing, like this is about React's internal jargon. It's stuff that you may be interested in, like me, if you're curious. You do not need to know this stuff at all to build great apps with React.
React is the best way to build user interfaces on the Web. And you don't need to know the internals for that. Who wants to dive into the internals today just for fun? Yes, that's right. Let's get this. Let's get this show on the road. We're going to be talking about something called fiber. Fiber is something that I don't have a lot of on my... But what is it in the React context? Succinctly put, fiber... And by the way, we're going to be diving into the fiber architecture in 20 minutes. Imagine that! Fiber in React is a single unit of work. There's a single unit of work that React does. And you might be thinking, OK, what is the work? The work is essentially rendering your stuff and putting it on the screen. Render and commit. Fiber is an internal thing. Again, so you don't need to know about it at all to build great React apps. And as such it's not exposed to the users. So maybe an easier way of reasoning about it is to think of something that is exposed and go from there. OK? What is exposed in React? The humble element. We all write React elements. We write an angular bracket with a name and that calls react.create element. And we have elements. Elements are what we work with daily. And I'm not going to dive deep on elements because I did that once. That was a relatively popular talk about React elements and how they exist and how they are created and all this. Feel free to watch that in your free time. But what I want to do is compare and contrast elements and fibers so we get, you know, some sense of how they work together. Essentially what I want to summarize that talk in saying for this talk is a React element describes desired UI state. A React element allows you to describe, like, declaratively, here's my tree of React elements. This is my app. And then React does the work of making that the truth in a host environment. So in a browser, in a mobile device with React Native, in a command line interface with React, so your elements describe what you want. React makes that happen. Is that clear? Awesome. So how do fibers come into this picture? For starters, elements are public, meaning they're external. We write them. But fibers are internal. We don't talk about them because we don't need to. There's a whole team that works with these daily for our sake, on our behalf, so we can build great apps. Elements are ephemeral, so they don't live long. You can throw away elements. You can create new ones.
2. Understanding Elements and Fibers
Elements are ephemeral, while fibers are long-lived, stateful data structures used by React to track app behavior and appearance. Fibers facilitate efficient updates to elements, preventing unnecessary re-rendering. Reconciliation is the process of aligning the UI description with the host environment, and fibers play a crucial role in this.
They're just a representation. If you speak DevOps, they're like a Kubernetes manifest. You just describe the state you want, and then a system brings it to fruition. Or like a Terraform file. Any DevOps people in the house? Oh, cool. Hey. One person.
Elements are ephemeral. Fibers are long-lived, stateful data structures that React uses to keep track of what your app is supposed to do. What you want it to do, and what you want it to look like. And that is React's value proposition at all.
So the way to build user interfaces that makes efficient and proper updates to your elements such that it doesn't throw one out and give you a new one every time. Can you imagine losing your input focus state on keystroke? Nasty. So fibers facilitate that. And third, elements are immutable. Meaning, you don't change them. You throw them away. You create new ones. But fibers are long-lived, mutable, stateful data structures used in the process of reconciliation. What is reconciliation? Reconciliation is when you take a tree of elements, a description, a road map of what you want your UI to look like and how you want it to respond to events and what effects you want it. You take this list of elements and reconcile them to a host environment. The browser or a mobile device or CLI. That is reconciliation. It's essentially taking your description and making it the truth. Fibers are instrumental in this.
3. Understanding Fiber Loop and Rendering Process
Now, we understand how elements become fibers and the interplay between them. The work is done in the fiber loop, which sets flags and completes work on the tree of fibers. Complete work builds DOM nodes in a detached state, and the commit phase puts everything on the screen. The ability to pause and interrupt rendering is the whole point of Fiber.
The work is done in what is called the fiber loop. Think of it as a game loop. When there's work to be done, you start a loop at the top of your tree of fibers. You begin work. These are function names in the source code. You begin work on the topmost node. And what begin work does is it essentially sets flags. Should this thing be updated? What changed? It sets a bunch of flags. And when it's finished, it steps into the next node in your tree. And work begins on that. The next one being the child. Begin work sets a few flags on that. If there's a child, it just keeps going down, setting flags until there's no more children and then what happens? Complete work. Now complete work does something else. It also will then step into the sibling as the next one. Not the child. If there is a sibling. And then go back up the tree, completing work. Complete work kind of builds DOM nodes, but not in the DOM, in a detached state. So you don't see any of this on your screen. It kind of just connects nodes together. And finally you have the commit phase. Which is putting everything on the screen. So all of, up until commit root, nothing happens on the screen. And the reason for this is, if rendering work needs to be interrupted or paused, it's not while the user is watching stuff happen. You can throw away a tree and recreate it off the screen and then commit it finally. That's the whole point of Fiverr, is the ability to pause and interrupt rendering and prioritize updates. Some has high priority, some has low priority. But I've been talking too much and honestly I don't like designing slides. So let's just draw a diagram and look at some code. Is that cool? Do I have permission? Okay. That is nice. One woo. Let's get this. So I have Excalidraw. Shout out. Who likes Excalidraw? Yes. Awesome. Vizhou, thank you for this tool. So let's go in here.
4. Drawing Tree of Elements and Fiber Reconciliation
We draw a tree of elements, including a main, H1, div, span, and button. The fiber reconciler maintains two trees: the current tree and the work in progress tree. When the plus button is clicked, the fiber reconciler updates the work in progress tree in a detached way and then commits the changes. It starts with the main node, sets flags, and moves through the tree, marking updates and completing work.
And what we're going to do is we're going to draw a tree of elements. So we have a main. As a child of main, let's say we have H1. Actually, let's put a div. Div soup. My favorite breakfast. H1. And we have some siblings to H1. So let's say here we have a span and we have a button. Okay. Is there a clock? I really don't know how I'm doing on time. But that's good. Don't show me the clock.
So, all right, so we have an H1. And these have some text underneath them as well. So H1, let's say this is hello world like that. The span, let's say this is a counter app where we're just drawing stuff. And, of course, let's say this is just a one-way counter. All you can do is increment. If we could do that with my salary, I'd be really happy. Cool. So this is a tree of let's say elements, let's say five, whatever, just a tree.
How does the fiber reconciler work? Now, at any given time, the fiber reconciler maintains two trees. So step one, let's duplicate this. Let's duplicate this tree. Let's first wrap it up in a nice little box, let's go here, and then we're going to duplicate this whole thing. So we'll just boom, second tree, clone, let's go. So now we have two trees. The first one is called the current tree, second one, let's call it the work in progress tree. And this is where a lot of the rendering work will do. So now the question, what would the fiber reconciler do if I click the plus button and increment the count? That's what I want to talk about. And how does it do that? So I click plus. What happens is we work on the work in progress tree in a detached way from the host environment, the browser, make any updates and then commit it. And this is how it happens. So when we click the button, we start the work, so to speak. And so you call begin work on the top most node that is the main, and this will just set some flags, mark it as this thing needs to update or maybe it doesn't. In this case, I don't know if it does because its content is changing, but anyway. Begin work sets some flags. And when it's finished, it steps into the div, begin work on the div. Same thing, mark any updates required. Just tell react what the state is. And then when that's finished, step into the child here, h1. Same drill. Begin work, mark it for updates, move on. Now, there's no children here. What happens? We complete work. There's no children. No more children to step into. We call a function called complete work and if there's no siblings, we go back up the tree. If there is siblings, we go to the sibling.
5. Understanding Fiber Work and Implementation
We complete work on the fiber tree by marking complete work and moving to the sibling. After completing work on the children, we go back up to the root. Complete work constructs a detached tree of DOM elements and connects everything. The work in progress is then put on the screen using the fiber root node. When the work loop finishes, we switch to the commit phase, where effects are run and the UI updates. Fiber's implementation can be understood by looking at the code and the tree structure.
We complete work here. How do I mark complete work? I'm just going to say complete work is going back this way. And then go to the sibling. Here to the span. We begin work on the span. Do we have children? Do we not? We have text nodes and this is probably interpolated, so this does count as a child. So begin work will go in there, complete work because there's no children, come back, and we just walk. We walk, walk, walk. And after we're done with these children, we complete work on the ones that have not completed work yet. So we'll call it complete work and go back up to the root. That's the cycle.
What does complete work do? Begin work sets flags, complete work constructs a detached tree of DOM elements, but not in the browser. That's props. Builds this tree, constructs, connects everything. And then we're done. This is where we are. The next step is to put this work in progress thing on the screen. To do that, react has a hidden internal structure called a fiber root node. You don't see this thing, but it exists. And at the time of your app mounting, it will point to the current tree. This is before updates. This is the state. But then when the work loop finishes, when complete work finishes on the last thing, we finish the work of rendering and switch to the commit phase. We commit to the main host environment, browser, by flipping the pointer. Now that is the source of truth. And also the commit phase will run any effects. There's different kinds of effects. There's host effects, which is literally like attaching the dom nodes to the browser. There's passive effects, which is your use effect calls and So the effects run, the dom node, both passive and host effect, they all run on commit and then your UI updates. That is how fiber works. By diagram. Cool?
How does fiber work by code? Let's, I really don't know how I'm doing on time. Let's just look at code, whatever. I don't see a clock. When you turn on a clock, I'll stop. How does this work in code? Let's open code and what I'm going to do is literally open code and we will yarn parcel index. So guess what? I built this counter app, this one here, I built that as a demo just for funsies, okay? I'm going to open this here and what we have is this, which is literally the same thing as this. In fact, if we open the code, did we open the code? So if we open the code, look at this tree. It's literally main div, H1, span button. It's the same structure, okay? But in code. If you've written react, this kind of looks familiar. If we open this now, it's open. How do I open? Okay. This works as you expect. Let's get into the react DOM source code and play. You can tell I really don't like slides. I just like playing. So what we'll do is let's go here. Literally node modules. Uh-oh.
6. Exploring React DOM Development
We're going deep into React DOM development. The source code is bundled, but easier to read on GitHub. Let's explore the work loop sync and its functions. Perform unit of work stores state and begin work wraps another function. The current fiber is compared and never mutated, while the work in progress fiber is the one we work with.
We're going deep. Where is react DOM? There it is. Common JS? Where is it? React DOM development. We're here. We're here, everybody. We've entered. Look at this. The number of foldable regions is limited to a maximum of 5,000. It's not this complex. It's just bundled. The source code is much less bundled for things. It's easier to read the GitHub.
We want to start with the work loop sync. That's what we had in our diagram. Let's command F. We have that. What does this function even do? Let's go to definition. So while there is work to do, perform it. It's declarative. I love it. What does that do? Let's go deeper. It's like the fish theme. This fish, we want to find the fish. If you believe that, tweet find the fish. Anyway, perform unit of work. What does this do? It stores some state. If we're in profiled mode. We're not in profiled mode. We're in begin work. Let's go deeper. What is begin work? It's a function that wraps another function. Let's go to this one. Begin work. We've reached the zone. There's three arguments. That's the one in the host environment. We don't touch this. We pass it around. The current fiber is we send it here, we send it there. We compare it. We never mutate. It's not. But the work in progress fiber is the one we work with because it's work in progress. So let's start by console. Log. Let's look at a fiber. Everybody, you're about to look at a fiber. Work in progress. Let's do this. Now let's open this.
7. Understanding Begin Work and Complete Work
Go to the console. We're working on the fiber root node, which is the hidden thing we talked about. Begin work compares old props with new props or context changes and sets flags for updates. Complete work follows a similar process, and we complete work on the fiber root node.
Go to the console. Okay, so there we have. Let me bump the font size for you. On what. The state node is what we're working on. So we're working on the fiber root node. That's that hidden thing we talked about in the diagram. We begin work one level lower on the app component, one lower on main. This is what a fiber looks like, by the way. The state node is the actual, like, element that is stateful. The stateful node. You can see it literally begin work and traverse the tree. So main, div, span. It begins work going down. And let's look at what begin work actually does. So this is a debug internal thing. We won't talk about that. But if the current node is not null, meaning if the current node in the host environment exists, then what? It's an update. Not an insert. Okay? So on the update path, this is what begin work does. We compare old props with new props or has context change. We set some flags. And if I scroll, we're just setting flags, making sure the reconciler knows this thing needs to update or needs to not. That's it. As we scroll down, we have a massive switch case for different kinds of components, lazy components, function components, class components, suspense boundaries, et cetera, that just marks them as this thing needs to update. As we scroll past the big switch case, we eventually end up at a throw where unit's like I don't know what you're Trying to do, but I can't work on this thing. And that's begin work. Now let's look at complete work. Let's go back up the tree. Complete work, as you notice, the same signature, render lanes is a scheduling concern that is outside of the scope of this talk, so we won't address that. We still have the current fiber, the work in progress fiber. For posterity, let's log this work in the complete work fiber. And we'll just console log the work in progress fiber here. And now when this thing reloads, we'll see, oh look, it got more complex. So begin work. Begin work. We keep beginning work until we reach H1, right? Now H1 has no children. H1 is a sibling. If we look at the DOM tree here, H1, no more children. A text node, but that is not an element. It's just text. So we begin work on H1. As you can see here, element type H1. And we complete it straightaway. We begin work on the span, but notice there's more children here, because that's because I have this interpolation here. Okay, so they're treated a little bit differently. But we begin work, and if there's no children, we complete work. And ultimately we complete work on the button, the div, the main, the app component. Lastly, we complete work on the fiber root node as well. And then, here you can see here, the state node is the fiber root node.
8. Understanding Complete Work and Commit Phase
Exactly. That's that invisible thing. That switch is a pointer. A host component is a component in the host environment. For React DOM, the host environment is the DOM. On a host component, you bubble properties. Complete work constructs the DOM node, attaches it to the parent, moves it back up. We then switch to the commit phase by calling commit root, which flushes passive effects and switches the pointer.
Exactly. That's that invisible thing. That switch is a pointer. So we've looked at, let's look at what complete work does. So if we go back here, it's a massive switch case again. Right? For all of these types of components, bubble properties. And that's exactly what it sounds like. And I want to focus here on one specific thing called the host component. A host component is a component in the host environment. For React DOM, what is the host environment? It's the DOM. The browser. On a host component, how do you complete work? This is how. You bubble properties. You check if it was hydrated from server side rendering. But in this case, it's not. We're just client side rendering. We'll switch to the other branch. Go here. And instance is create instance. You know what that is? Let's go one level deeper. Create instance is essentially a document that validates some nesting and calls create element. And now we're at what complete work does. It's basically creating an element off, like detached from the browser, but it's still constructing a tree. Complete work constructs the DOM node, attaches it to the parent, moves it back up. Okay. Just traverses the tree. And you can actually see that. We append all the children, we set the state node to the instance, and we mark some updates, et cetera, and we just keep going in the switch case. Complete work eventually makes it to the root and rendering is finished. We then switch to the commit phase. We put stuff on the screen. We switch. If we go back to the diagram, what the commit phase is, if you remember, is switching the pointer, like that. How do we switch the pointer? There's a function called commit root. Yeah, that's the one. Let's go deep into this and see. What I want to do is console.log, commit root. And we'll log the root here, but preferably like that. Save. So now you see, we've committed the root. It literally just switches the pointer. This is the fiber root node that you're seeing. Every time I call this, it just does the same thing over and over again really, really fast. What does commit root do? It flushes passive effects. A passive effect, look, there's even an explanation here. A passive effect is not a host effect. It's not like attaching stuff to the DOM. In host effects, there's different kinds. There's an insertion effect, there's a placement effect, there's an update effect, these effects literally involve mutating the DOM and there's passive effects which are zero component effects. So we flush those things, we mark some events, and we eventually switch the pointer here.
9. React Internals and Understanding the Code
So we just run, there's a lot of effects running, really, a lot of effects being committed. But at some point what you'll see is we switch the trees from the current to the work in progress. React internals are complex, but the externals are powerful because of that. Understanding the internals has helped me in cases of pull request reviews where we have debates or something. I stay away from micro-optimizations because I know what's happening. It helps inform how much do I optimize.
So we just run, there's a lot of effects running, really, a lot of effects being committed. But at some point what you'll see is we switch the trees from the current to the work in progress. And we also call a hook. When it's finished, what happens? Eventually, we mark the commit as stopped, and it finishes where all the last things finish. So what I want to show you with this is, one, how it works with the loop, but also that there's a lot of edge cases, there's a lot of stuff that React solves for you that you don't need to solve yourself, and that's the whole value proposition, how do we make updates to the UI in a fast way, in a predictable way, and in a way that allows us to just build apps fast, without concerning ourselves too much with the internals.
That cool? All right. Let's talk about my takeaway. What can we take away from this talk? We can take away that React internals are complex, but the externals are powerful because of that. Like, we can build all of those edge cases, those massive switch cases you saw, our work that the team does on our behalf, so that we don't have to, so that React can have a really good environment to build apps powerfully and fast.
That said, the internals are fun. And my one takeaway for you is to remember, hey, listen, React, I've heard a lot of people say that React is too hard or too complex, like the internals. All of this talk I prepared was by reading the source code on GitHub because it's open source and trying to participate, and actually, now I'm participating, I guess, I did talk to some of the team who are very welcoming and supportive, and it's an ecosystem. So my takeaway is, I'm thankful to be a part of this ecosystem and I hope you will be encouraged to do so as well. React Advanced London, thank you so much for the time. Thank you, thank you, thank you. Please do leave your laptop and follow me to the interrogation chamber. Uh-oh. Yes. I'm about to be interrogated. You're in trouble, young man. No, thank you, that was a deep dive. You are truly living up to your promises and the name of this conference. Am I a fish? Are you a fish? Do you want to be a fish? Is being a fish a good thing? There's a superhero in this show called The Boys who's a fish. The deep, anyway. Yeah, I, Aquaman is underrated is my take. That's my take. Should we talk more about that? No, okay. Let's go to audience questions. The first audience question is, in what ways have you found that a deeper understanding of React under the hood has informed or changed the code you write? I like this a lot. I like this a lot. So in small ways. And I cannot emphasize enough how important it is to not concern ourselves too much with the internals. There's people paid well to do that for us, right? And so they do it well for us so that we don't have to, and the focus is using React. And that's the focus. Not necessarily understanding the internals. But understanding the internals has helped me in cases of like pull request reviews where we have debates or something, we can actually like look at the code and I can teach and mentor. I think it helps. But also, I stay away from micro-optimizations because I know what's happening. Like, I don't need to put everything in used memo. I don't need to make all my components lazy. Like, it helps inform how much do I optimize because I know how much is being optimized for me behind the scenes, so. For sure. I want to add my own question to that. I remember when we used to work with simpler tools that weren't as powerful, but also were simpler. At one point, I think I remembered most of the backbone source code by heart. Now with these tools that we use right now, the complexity is too much for a working product programmer to kind of look at. So, how much do you think is important as you become, let's say, more senior in organization to have a person on a team who understands these tools? Or is that just a completely waste of time? I don't know if it's a waste of time, because I think a big important part of being a senior engineer is the ability to mentor. There's a soft skills room, right? I really want to attend that because I feel like that's actually more important than the code itself sometimes. And so I think if there's at least one person who has a real depth of knowledge there, they can use it to educate and inform. And there's a lot of patterns in this code base that I can learn from personally.
10. React 18 and the Work Loop
The work loop sync and work loop async functions in React 18 facilitate concurrent features and a priority system. While the code shown is still in React 18 and hasn't changed, work loop async may become the default in the future. If an element is removed directly via the document object, it can lead to issues with React's reconciler. It is recommended to use refs instead. XcaliDraw is the drawing tool used, but there are other great alternatives like tlDraw and stately.ai.
Just for example, right? This function called work loop sync. While work to be done, do work. This way of structuring code is something I can learn and then teach my team. So I don't think it's useless. I also think if you're a company in this economy, it's a nice to-have. But I don't think it's mandatory. Cool. The next top question is, did React 18 change anything in this algorithm? That's a good question. What I was using, I'm pretty sure, was React 18. Is React 18 at the latest tag? That was React 18, so no maybe, but there is a... So there was work loop sync that I talked about. There's also a work loop async. That's the concurrent feature. And that uses, as far as I know, this is where it gets shaky, but that uses a scheduler under the hood that literally will compute what's a high priority update, what's not, and the whole point of Fiber is to be interruptable. So switching from work loop sync to work loop async will facilitate a lot of these concurrent features where there's now a priority system and a scheduler for those. The code I showed is still in React 18 and it hasn't changed, but at some point, I suspect, work loop async will be the default and work loop sync will be the fallback. Yeah, hopefully next year we can have you back and you can give a talk about the async scheduler. Yes. I think that's the part for me that is still a little bit of black magic. I understand that it works, but I'm not really sure how at all. He's giving me ideas for my next talk. You want to see that? No. No. Me neither. Me neither. But that was that one guy. You should talk to him next. Yeah. All right, top question. Let's go by democratic order. This feels like almost like a job interview question. If an element is removed by a document object directly, so I'm assuming manipulating the DOM, is there a possibility to cause a memory leak since the Fibre keep the reference? I don't know how Fibre keeps references. I haven't looked into that, but yes. You know the stack reconciler before the Fibre one. That's how it used to be, right? Things are mutated on the fly. The problem with that is you can't bail out of something halfway. If you just update half the DOM, and you're like, higher priority update came in, what do you do? You can't roll it back. Should you roll it back? You can't. So it becomes complicated. So if an element is removed via the document object directly, you basically go back to React like pre-16, which there's a reason that that was deprecated. And I think if the element is maintained by React itself, I think the next work loop will either be added or crash in an undefined way. So that's probably a bad idea, and you should not do that. Use refs. Yes. That's part of complete work as well as updating refs. Cool. Top question, of course, not what font did you use, but what is the name of the drawing tool you used? XcaliDraw, I used it because I like it, but great alternatives, tlDraw, great tool. Yeah, we got a tl, and also stately.ai for diagrams. Excellent. Really, we have no shortage of awesome tools here.
11. Differences and Benefits of Fiber
Fiber brings several differences over the old reconciliation heuristics. It allows for async rendering, scheduling, and the ability to do half-jobs and discard work-in-progress trees. This was not possible with the previous stack reconciler, which often caused low priority updates to block user input.
Cool. We have time for one or two more questions. Let me take this one. What differences do Fiber bring over the old reconciliation heuristics? I mean, I guess ability to do async rendering and all of that is a future thing, but is there anything else you'd like to add? I think scheduling, really. This is a big one. Like, the ability to do half-jobs and then discard a work-in-progress tree, that's cool. You could never do that before. There was no way to do half-jobs, and so, because you couldn't do half-jobs the previous way, the stack reconciler, which I think that's the question. I forgot the question. I'm just freestyling. You can't do half-jobs, so what would happen was a really low priority update, like some image thing that maybe wasn't important would block a user input problem. And then Dan Abramov gave this JSConf Iceland talk about CPU-bound and I O-bound, and I was like, wow, that's awesome. Anyway.