Understanding React’s Fiber Architecture

Bookmark

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.

by



Transcription


Hi, 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. But 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? To 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, okay, what is the work? The work is essentially rendering your stuff and then 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, okay? 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.createElement 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. It was a relatively popular talk about React elements and how they exist, how they're 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 some sense of how they work together. Essentially, what I wanted to summarize that talk in saying for this talk is a React element describes desired UI state. A React element allows you to describe 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 Inc. 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, 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, right? 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. That is React's value proposition at all, right? It's a 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 roadmap of what you want your UI to look like, and how you want it to respond to events and what effects you want it to have. You take this list of elements and reconcile them to a host environment. Whether it's the browser or a mobile device or a CLI. That is reconciliation. It's essentially taking your description and making it the truth, okay? Fibers are instrumental in this. Now, we get elements, we get fibers, we get reconciliation. How do elements become fibers? Do they become fibers? What is the interplay? There's a function in source code, thank goodness React is open source and MIT licensed, I can just go read. There's a function called create fiber from types and props. Arguably, you could just say this is called create fiber from elements. Because if you've looked at a React element, it's a JavaScript object with a type, properties, children's part of properties sometimes, it's an object. So you can say a fiber is almost derived from elements and then used by the fiber reconciler. We understand reconciliation, we understand fibers, it all comes together in the fiber reconciler to do the work of building your UIs. The work is done in what is called the fiber loop. The fiber loop, think of it as like 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, it essentially sets flags. Should this thing be updated, what changed, et cetera. 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 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 Fiber, is the ability to pause and interrupt rendering and prioritize updates. Some has high priority, some has low priority. But you know, I've been talking too much and honestly I don't like designing slides, so let's just like draw a diagram, 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. Viju, thank you for this tool. So let's go in here 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 an 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. 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 for 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. Okay. 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 start, when we click the button, we start the work, so to speak. And so you call begin work on the topmost 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. Anyway, begin work sets some flags. And when it's finished, it steps into the div, begin work on the div. Okay. 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, right? Begin work, mark it for updates, move on. Now, there's no children here. What happens? We complete work. There's no children, there's 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. So we complete work here. And 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. Okay. So we'll just call 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, aside. Sets props and just like append child to each other. 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, it just contains a pointer to the first, 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 we switch to the commit phase. And we commit the work in progress to the main host environment, the browser, by just flipping the pointer. And 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 everything your components need to do. So the effects run. The DOM, 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, but let's figure it, let's just look at code, whatever. I don't see a clock. When you turn on the clock, I'll stop. How does this work in code? Let's open code. And what I'm going to do is literally like 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. 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, but in code. If you've written React, this kind of looks familiar. If we open this now, let'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. We's going deep. Let's go, where is React DOM? There it is. Common JS, where is it? React DOM development. Okay. 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. Anyway, so what do we want? We want to start with the work loop sync. That's what we had in our diagram. So let's command F. We have that. What does this function even do? Let's go to definition. So while work in progress is not null, while there is work to do, perform it. It's declarative. I love it. Okay. What does that do, though? Let's go deeper. We're going so deep, man. It's like that fish theme, you know? This fish, we want to find the fish. If you believe that, tweet find the fish, hash React. Anyway, perform unit of work. Okay. What does this do? It stores some state, and if we're in profile mode, we're not in profile mode. Profiling is performance. We're not doing that. We're here. We're in begin work. We call begin work. Let's go deeper. What is begin work? It's a function that wraps another function called begin work, but this is, let's go to this one. Okay. Begin work. Here we are. We're at begin work. We've reached the zone. There's three arguments. The current fiber, that is the one in the host environment. We don't touch this, okay? We just pass it around. So the current fiber is like, we send it here, we send it there, we compare it, we never mutate. We never, just 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, let's just console log. Let's look at a fiber. Everybody, you're about to look at a fiber. Work in progress. All right, let's do this. And so now let's open this. Let's go to the console. Okay, so there we have, let me bump the font size for you. So begin work, 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, then one level lower on the app component, one level lower on main. This is what a fiber looks like, by the way. Lots of state for React to keep track of. The state node is the actual, like, element that is stateful. The stateful node. And you can see it literally begin work and traverse the tree. So main, div, h1, span. It just 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. Okay? And that's begin work. Now let's look at complete work. Let's go back up the tree. So 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, oh sorry, complete work, 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, type H1, and we complete it straight away. We begin work on the span, but notice there's more children here because that's because I have this interpolation here. So they treat it 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, exactly. That's that invisible thing that switches a pointer. So 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. So 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. So we'll switch to the other branch in this if. So we go here, and look, instance is create instance. You know what that is? Let's go one level deeper. Create instance is essentially document that validates some nesting and then calls create element, which is document.createElement, and now we're at what complete work does. It's 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 back up. Okay? Just traverses a tree. And you can actually see that. So 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 just switching the pointer, like that. How do we switch the pointer? There's a function called commit root impl. Yeah, that's the one. Let's go deep into this and see. So what I want to do is console.log. Commit root, and we will just log the root here, but preferably like that. Save. And 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, and every time I call this, it just does the same thing over and over again really, really fast. Okay? 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-components effects. So we flush those things, we mark some events, and they eventually switch the pointer here. 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, right? And we also call a hook. When it's finished, what happens? Eventually, we mark the commit as stopped, and it finishes with all the last things finished. 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. All of those edge cases, those massive switch cases you saw, are work that the team does on our behalf so that we don't have to, so that React can have a relatively small API surface and allow us 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 the show called The Boys who's a fish, the deep anyway. 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. That helps. But also I stay away from like micro-optimizations because I know what's happening. Like I don't need to put everything in use 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. For sure. I want to add my own question to that. So 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 just 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 an organization to have a person on 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 so 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. 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 nice to have, but I don't think it's like mandatory. Yeah. 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? Yes. Yeah, 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 features. 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 like what's a high priority update, what's not. And the whole point of fiber is to be interruptible. 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 the 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 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, no. Me neither. But that was that one guy. You should talk to him. 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 fiber keep the reference? I don't know how fiber keeps references. I haven't looked into that. But yes, I mean, you know the stack reconciler before the fiber one. That's how it used to be, right? Like things are mutated on the fly. The problem with that is you can't bail out of something halfway. Like if you just update half the DOM and you're like, oh, higher priority update came in. What do you do? Like 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. Yeah, and I think if the element is maintained by React itself, I think the next work loop will either be added or crash in like 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. 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, woo, we got a TL. And also stately.ai for diagrams. Excellent. Really, we have no shortage of awesome tools here. 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 is the 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, is 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 IO bound, and I was like, wow, that's awesome. Anyway. Cool. All right, let's do a final question. I think we have a couple of questions on the same topic. I think this is a very reasonable question given the terminology change overall. So how does the concept of virtual DOM fit into this kind of fiber thing? Is the fiber the virtual DOM? What is the relationship between these two terms? That's a good question. As far as I know, fallible knowledge. As far as I know, the virtual DOM is just a virtualized, it's just like a data structure. It's a clone representation of your DOM, but for a library to do whatever it wants with, and then change tracking and diffing and all of that separate as far as I know. Cool. Well, that is all we have time for on stage, but anybody who wants to speak to Tejas can find him on the hallways or in the speaker Q&A room, where you can also join through spatial chat if you're joining us remotely. Thank you very much, Tejas. Let's give Tejas one last big round of applause.
30 min
21 Oct, 2022

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Workshops on related topic