Fiber Architecture: Why React Rewrote Its Rendering Engine
Before React 16 shipped, the React team spent nearly two years secretly rewriting the entire rendering engine. The rewrite changed no public APIs, yet fundamentally transformed how React works internally. Understanding Fiber architecture is the prerequisite for understanding every advanced React feature — concurrent mode, Suspense, useTransition — all of them are built on this foundation.
The Fatal Flaw of the Stack Reconciler
React 15 and earlier used a rendering engine called the Stack Reconciler. Its operation can be summarized in one word: recursion.
When you called setState to trigger a re-render, the Stack Reconciler would start from the root node, recursively traverse the entire component tree, compare new and old virtual DOM nodes, generate update operations, and then commit everything to the DOM in one pass. This process was synchronous and uninterruptible.
// React 15 Stack Reconciler core logic (pseudocode)
function updateComponent(instance, nextElement) {
const prevRenderedElement = instance._renderedElement;
const nextRenderedElement = instance.render();
// Recurse downward — cannot be paused
updateComponent(
instance._renderedComponent,
nextRenderedElement
);
}
This recursive invocation consumed JavaScript's call stack. When the component tree was deep and dense, a single complete render could take 100ms or more. During that time, the main thread was fully occupied — the browser could not respond to user input, could not execute animation frames, and the page became completely unresponsive.
Browsers typically refresh at 60Hz, meaning one frame every 16.6ms. If JavaScript execution exceeds that threshold, users perceive dropped frames and jank. For a complex React application, this was practically unavoidable.
The Stack Reconciler's problem was not algorithmic inefficiency — it was architectural uninterruptibility. You cannot "pause for a moment to handle a user click, then resume" in the middle of a recursive call. JavaScript is single-threaded; once a call stack starts, it must run to completion.
Fiber: Redefining Work as a Linked List
The React team's solution was to abandon recursion entirely and replace it with an interruptible loop. This required decomposing the monolithic task of "rendering a tree" into individual, independently executable small units of work.
Each unit of work maps to a Fiber node. The entire Fiber tree is no longer a nested object structure but a linked list connected through pointers — specifically, a tree-shaped linked list represented by three pointers: return, child, and sibling.
The critical advantage of this data structure is: traversal can stop at any moment; you only need to remember which node you were processing, and next time you can resume from exactly that node.
The FiberNode Data Structure
Here is the core structure of FiberNode from React's source code, located in packages/react-reconciler/src/ReactFiber.js:
// Simplified from packages/react-reconciler/src/ReactFiber.js
function FiberNode(tag, pendingProps, key, mode) {
// Identifies the node type (FunctionComponent=0, ClassComponent=1, HostRoot=3, HostComponent=5...)
this.tag = tag;
this.key = key;
this.elementType = null; // Original type from JSX (e.g., the function reference)
this.type = null; // Resolved type (may be unwrapped from lazy())
this.stateNode = null; // The corresponding real node (DOM element or class instance)
// Fiber tree structure: three core pointers
this.return = null; // Points to the parent node (note: not called "parent")
this.child = null; // Points to the first child node
this.sibling = null; // Points to the next sibling node
this.index = 0; // Position among siblings
// Props and State
this.pendingProps = pendingProps; // The new props for this render
this.memoizedProps = null; // The props from the last completed render
this.updateQueue = null; // Queue of updates (from setState calls)
this.memoizedState = null; // State from last completed render (head of Hooks linked list)
this.dependencies = null; // Context dependencies
// Mode flags (Concurrent, StrictMode, etc.)
this.mode = mode;
// Effect flags
this.flags = NoFlags; // Operations needed on this node (Placement, Update, Deletion...)
this.subtreeFlags = NoFlags; // Aggregated flags from the subtree (React 18+ optimization)
this.deletions = null; // Child nodes to be deleted
// Scheduling priority
this.lanes = NoLanes;
this.childLanes = NoLanes;
// Double-buffer pointer
this.alternate = null; // Points to the corresponding Fiber node in the other tree
}
Understanding this structure is central to understanding how React works. Several points deserve careful attention:
return rather than parent: React names the parent pointer return, hinting at its intended semantics — when a Fiber node's work is complete, control should "return" to the parent to continue processing. This closely mirrors the return semantics of a function call stack.
stateNode meaning varies by node type: For host components (div, span, etc.), stateNode is the corresponding real DOM node. For class components, stateNode is the class instance. For the root node (FiberRoot), stateNode is the FiberRootNode.
memoizedState is the entry point to the Hooks linked list: For function components, memoizedState is not a simple state value — it is the first node of the Hooks linked list. Each useState and useEffect call corresponds to one node in this list.
Double buffering via alternate: The alternate pointer implements double buffering. React maintains two Fiber trees simultaneously — the "current tree" currently displayed on screen, and the "work-in-progress tree" currently being built. Corresponding nodes in both trees point to each other through alternate. When rendering completes, the two trees swap roles.
Tree Traversal: Depth-First Linked List Walk
Fiber tree traversal is no longer recursive but rather a depth-first loop. The traversal follows these rules:
- Starting from the root, explore downward first (
child) - When there are no child nodes, process the current node, then explore the next sibling (
sibling) - When there are no more siblings, follow
returnback to the parent and continue its siblings
This process can pause at any node — simply save the current workInProgress pointer, and next time resume directly from there.
// packages/react-reconciler/src/ReactFiberWorkLoop.js (simplified)
function performUnitOfWork(unitOfWork) {
const current = unitOfWork.alternate;
// beginWork: process this node, return child node (if any)
let next = beginWork(current, unitOfWork, renderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// No children — complete this node
completeUnitOfWork(unitOfWork);
} else {
// Has children — return child as next unit of work
workInProgress = next;
}
}
Two Work Loops: Synchronous and Concurrent
React provides two work loop modes. Their only difference is whether to check after each unit of work whether control should be yielded.
// packages/react-reconciler/src/ReactFiberWorkLoop.js
// Synchronous mode: uninterruptible, runs to completion
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// Concurrent mode: after each unit of work, checks whether to yield
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
shouldYield() comes from the Scheduler package and checks whether the current frame has remaining time. If time runs out, workLoopConcurrent stops the loop, returns control to the browser, and waits for the next frame to continue.
This is the essence of concurrent mode: rendering work is distributed across multiple frames, doing only a portion per frame.
Two Phases: Render and Commit
The entire update process is divided into two distinctly different phases:
Render Phase (Reconciliation Phase)
The render phase builds the new work-in-progress Fiber tree and determines which nodes need to be updated. This phase:
- Can be interrupted: Uses
workLoopConcurrent, can yield control at any time - Has no side effects: Does not modify the DOM, does not execute side-effectful lifecycle methods
- Can be discarded: If a higher-priority update arrives, current work can be thrown away and processing starts fresh for the new update
The render phase is driven by two functions, beginWork and completeWork:
beginWork: Processes a node, creates or reconciles child Fiber nodes, marks appropriate flagscompleteWork: After a node's work is done, creates real DOM nodes (but does not insert them), bubbles flags up to the parent'ssubtreeFlags
Commit Phase
The commit phase applies the results of the render phase to the real DOM and executes all side effects. This phase:
- Cannot be interrupted: Must complete synchronously, cannot be paused
- Has side effects: Manipulates the DOM, calls
useLayoutEffect,useEffect, lifecycle methods, etc. - Has three sub-phases:
// packages/react-reconciler/src/ReactFiberCommitWork.js (simplified)
function commitRoot(root) {
// Sub-phase 1: BeforeMutation (before DOM mutations)
// Calls getSnapshotBeforeUpdate, schedules useEffect
commitBeforeMutationEffects(root, finishedWork);
// Sub-phase 2: Mutation (modifies the DOM)
// Performs insertions, updates, deletions
// Calls useLayoutEffect cleanup functions
commitMutationEffects(root, finishedWork, lanes);
// Switch the current tree
root.current = finishedWork;
// Sub-phase 3: Layout (after DOM mutations)
// Calls useLayoutEffect setup functions
// Calls componentDidMount / componentDidUpdate
commitLayoutEffects(finishedWork, root, lanes);
// Asynchronously schedule useEffect
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
});
}
The reason the commit phase cannot be interrupted is straightforward: DOM operations must either all happen or not happen at all — a half-applied update would leave the user seeing a broken interface state.
React 18 and React 19 Evolution
React 18 refined concurrent features built on the Fiber architecture. React 19 further optimized the scheduling and commit phases.
In React 19, the subtreeFlags calculation logic was optimized to reduce unnecessary subtree traversals. React 19 also adjusted the memory layout of Fiber nodes, placing fields on the hot path closer to the object header to take advantage of V8's Inline Cache optimization.
// In React 19, FiberNode creation uses object literals in some scenarios
// This helps V8 better optimize object shapes (Hidden Classes)
function createFiberImplObject(tag, pendingProps, key, mode) {
return {
tag,
key,
elementType: null,
type: null,
stateNode: null,
return: null,
child: null,
sibling: null,
// ... other fields
};
}
Once you understand Fiber architecture, many of React's design decisions become clear: why you cannot call Hooks inside conditionals (because the Hooks linked list must maintain a fixed order); why useLayoutEffect is synchronous while useEffect is asynchronous (because the former executes synchronously in the commit phase, the latter is asynchronously scheduled after commit); why concurrent mode can implement priority scheduling (because the work loop can interrupt and restart at any time).
Fiber is not merely an implementation detail — it is the material embodiment of React's concurrent philosophy.