React Fiber is a re-implementation of the React reconciliation algorithm, designed to improve performance and enable new features like concurrent rendering.
Let’s see it in action. Imagine you have a simple component that updates its state frequently:
import React, { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 100); // Update every 100ms
return () => clearInterval(intervalId);
}, []);
return <h1>Count: {count}</h1>;
}
export default Counter;
In a pre-Fiber React, updating the count state would trigger a synchronous, top-down traversal of the component tree. If this traversal took too long (e.g., due to a complex component or a slow browser environment), it would block the main thread, making the UI unresponsive. You might see jank, dropped frames, or even the browser freezing.
Fiber changes this by introducing a pluggable reconciliation algorithm that can pause, abort, and resume work. Instead of a single, monolithic render pass, Fiber breaks down the rendering work into smaller, interruptible units called "fiber nodes." Each fiber node represents a React element and contains information about its corresponding component instance, its children, and the work to be done.
When React needs to re-render, it creates a new "work-in-progress" fiber tree parallel to the current "committed" fiber tree. The reconciliation process then traverses this work-in-progress tree. Unlike the old model, Fiber doesn’t immediately commit changes to the DOM. Instead, it performs work in chunks. If an interruption occurs (like a user input or a higher-priority update), Fiber can stop the current work, process the interruption, and then resume the interrupted work later. This is the core of concurrent rendering.
The key to Fiber’s interruptibility is its linked-list structure. Each fiber node has pointers to its child, sibling, and return (parent) fiber. This allows React to traverse the tree in a depth-first manner, but also to easily jump between siblings or return to the parent. Work is prioritized: updates from user interactions (like typing) are higher priority than background computations. Fiber can preempt lower-priority work to handle higher-priority updates, ensuring the UI remains responsive.
Here’s how the internal structure looks conceptually:
// Simplified Fiber Node Structure
class Fiber {
tag: number; // Type of element (e.g., FunctionComponent, ClassComponent)
type: any; // Component function or class, or DOM tag name
key: string | null;
stateNode: any; // Instance for class components, DOM node for host components
// For traversal
return: Fiber | null;
child: Fiber | null;
sibling: Fiber | null;
// ... other properties like pendingProps, memoizedProps, updateQueue, etc.
}
When setCount is called in our Counter example, Fiber doesn’t immediately re-render <h1>. It schedules a "work-in-progress" update. The scheduler determines the priority of this update. In this case, it’s a relatively high priority because it’s driven by a state update.
Fiber then begins to walk the work-in-progress tree. It finds the Counter fiber, sees that its state needs updating, and calculates the new props. If the component is a function component, it calls the function. If it’s a class component, it calls render(). The result of this computation is a new set of React elements.
Fiber then compares these new elements with the existing ones (from the previous render, also represented as fiber nodes). It identifies the differences. If only the count value in the <h1> has changed, Fiber creates a DOM update instruction only for that text node. This diffing process happens on the work-in-progress tree before any DOM manipulation occurs.
The "commit" phase is when the actual DOM updates happen. Fiber batches all the identified DOM changes and applies them in one go. This is still synchronous but is now very fast because it only contains the minimal necessary DOM mutations.
The truly groundbreaking part is what happens when an interruption occurs. Imagine a user clicking a button on the same page that triggers a high-priority modal to open while the Counter is still being processed by Fiber. Fiber can pause the Counter update, process the modal’s rendering (which is higher priority), commit the modal, and then resume the Counter update from where it left off. The user experiences the modal appearing instantly, and the counter continues updating shortly after, without the UI freezing.
The one thing most people don’t realize is that Fiber doesn’t just make rendering interruptible; it fundamentally changes how React manages state and effects. useEffect and useLayoutEffect hooks, for instance, are now tied to the Fiber lifecycle. useLayoutEffect runs synchronously after all DOM mutations have been committed but before the browser paints, allowing for DOM measurements and synchronous DOM manipulations without causing visual inconsistencies. useEffect, on the other hand, runs after the browser has painted, making it suitable for asynchronous operations or side effects that don’t need to block rendering. This distinction is crucial for understanding how to avoid performance pitfalls in concurrent mode.
This ability to pause and resume work also underpins features like React.lazy for code-splitting and useTransition for marking certain updates as non-urgent.
The next logical step after understanding the Fiber architecture is to explore how it enables concurrent rendering and the implications of useTransition and useDeferredValue.