React Deep Dive
Chapter 2: Introduction to React Fiber
In the previous chapter, we saw how React's recursive rendering process could block the main thread and make the UI unresponsive. React 16 introduced a new reconciliation engine called Fiber to solve these problems. But what exactly is Fiber, and how does it work?
What is Fiber?
Fiber is React's new reconciliation engine that enables:
- Interruptible rendering
- Priority-based updates
- Concurrent rendering
- Time-slicing
At its core, Fiber is a reimplementation of the stack, but with a crucial difference: it can be interrupted and resumed. Let's understand why this is important.
Why Some Lifecycle Methods Are Marked as UNSAFE
Remember those lifecycle methods with the UNSAFE_ prefix I mentioned in Chapter 1? Now we can understand why they were marked as unsafe. The methods in question are:
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
These methods were marked as unsafe because Fiber's interruptible nature means they might be called multiple times during a single update. Here's why:
1// Example of a problematic lifecycle method
2class Component extends React.Component {
3 componentWillMount() {
4 // This might be called multiple times
5 // due to Fiber's interruptible rendering
6 this.setState({ count: this.state.count + 1 });
7 }
8}
In the old stack-based renderer, these methods were guaranteed to be called exactly once per update. But with Fiber:
- Rendering can be interrupted and resumed
- The same update might be processed multiple times
- These lifecycle methods could be called multiple times
- This could lead to unexpected side effects and bugs
This is why React team:
- Added the UNSAFE_ prefix to warn developers
- Introduced new lifecycle methods that are safe with interruptible rendering
- Recommended using the new methods instead
The Stack vs Fiber
In the previous chapter, we saw this recursive render function:
1function render(element, container) {
2 const dom =
3 element.type == "TEXT_ELEMENT"
4 ? document.createTextNode("")
5 : document.createElement(element.type)
6
7 const isProperty = key => key !== "children"
8 Object.keys(element.props)
9 .filter(isProperty)
10 .forEach(name => {
11 dom[name] = element.props[name]
12 })
13
14 element.props.children.forEach(child =>
15 render(child, dom)
16 )
17
18 container.appendChild(dom)
19}
This implementation uses the JavaScript call stack, which means:
- Each recursive call adds a new frame to the stack
- The stack must be completely unwound before we can do anything else
- We can't pause in the middle of rendering
Fiber replaces this with a linked list of fiber nodes. Here's a simplified version of what a fiber node looks like:
1type Fiber = {
2 type: any, // The type of element (div, span, etc.)
3 props: any, // The props
4 dom: HTMLElement | null, // The actual DOM node
5 parent: Fiber | null, // Parent fiber
6 child: Fiber | null, // First child fiber
7 sibling: Fiber | null, // Next sibling fiber
8 alternate: Fiber | null, // The fiber from the previous render
9 effectTag: string, // What to do with this fiber (PLACEMENT, UPDATE, DELETION)
10 lanes: number, // Priority of this update
11}
How Fiber Works
Instead of recursively calling the render function, Fiber breaks the work into small units and processes them one at a time. Here's a simplified version of how it works:
1function performUnitOfWork(fiber) {
2 // 1. Create DOM node
3 if (!fiber.dom) {
4 fiber.dom = createDom(fiber)
5 }
6
7 // 2. Create new fibers for children
8 const elements = fiber.props.children
9 let index = 0
10 let prevSibling = null
11
12 while (index < elements.length) {
13 const element = elements[index]
14 const newFiber = {
15 type: element.type,
16 props: element.props,
17 parent: fiber,
18 dom: null,
19 }
20
21 // Link fibers together
22 if (index === 0) {
23 fiber.child = newFiber
24 } else {
25 prevSibling.sibling = newFiber
26 }
27
28 prevSibling = newFiber
29 index++
30 }
31
32 // 3. Return next unit of work
33 if (fiber.child) {
34 return fiber.child
35 }
36 let nextFiber = fiber
37 while (nextFiber) {
38 if (nextFiber.sibling) {
39 return nextFiber.sibling
40 }
41 nextFiber = nextFiber.parent
42 }
43}
The key differences from the recursive approach are:
- Work is broken into small units
- Each unit can be processed independently
- We can pause and resume work at any point
- The browser can handle high-priority tasks between units
The Work Loop
Fiber uses a work loop to process these units. For this example, we'll use requestIdleCallback to demonstrate the concept, but React actually implements its own scheduler for better control and browser compatibility:
1function workLoop(deadline) {
2 let shouldYield = false
3 while (nextUnitOfWork && !shouldYield) {
4 nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
5 shouldYield = deadline.timeRemaining() < 1
6 }
7 requestIdleCallback(workLoop)
8}
9
10requestIdleCallback(workLoop)
This loop:
- Processes one unit of work at a time
- Checks if there's time remaining in the current frame
- Yields to the browser if needed
- Continues with the next unit in the next frame
In React's actual implementation, the scheduler is much more sophisticated:
- It handles priority levels for different types of updates
- It can interrupt and resume work based on priority
- It manages time slicing across multiple frames
- It provides better browser compatibility than requestIdleCallback
We'll explore React's scheduler in more detail in Chapter 4 when we discuss priority management.
Benefits of Fiber
This new architecture enables several important features:
- Concurrent Rendering: React can work on multiple versions of the UI simultaneously
- Time Slicing: Work is broken into small chunks that can be spread across multiple frames
- Priority-based Updates: Different updates can have different priorities
- Suspense: Components can "suspend" while waiting for data
- Error Boundaries: Better error handling with the ability to recover from errors
In the next chapter, we'll dive deeper into how Fiber handles updates and reconciliation.