React transformed modern web development by introducing a declarative component model. However, as web applications grew in size and visual complexity, React's original rendering engine faced a fundamental limitation: rendering updates was a synchronous, uninterruptible process that could block the main browser thread and cause UI jank.
With the release of React 16, the core engine was completely rewritten from the ground up under the project name React Fiber.
In this deep dive, we will explore the architecture of React Fiber, dissect its underlying tree structures, understand how time-slicing works, and examine how Fiber enables concurrent features like useTransition and useDeferredValue.
1. The Stack Reconciler vs. The Fiber Reconciler
To appreciate Fiber, we must understand the system it replaced: the Stack Reconciler.
Before React 16, React traversed the Virtual DOM tree recursively using the JavaScript call stack.
- Once a render started (
setState), React traversed the entire tree synchronously. - If a component tree contained thousands of nodes, the main thread was blocked for 50ms–200ms.
- User input (typing, clicking, animations) dropped frames because browser rendering and event handlers had to wait for React to finish.
Stack Reconciler (Pre-React 16):
[ Render Phase + DOM Updates ] -------- Synchronous execution (Main thread blocked) -------->
React Fiber replaced recursive call stacks with a custom linked-list data structure that allows React to split rendering work into small chunks, pause work to yield execution back to the browser, and resume or abort work based on priority.
Fiber Reconciler (Concurrent React):
[ Unit 1 ] -> Yield to Browser -> [ Unit 2 ] -> High Priority Event -> [ Unit 3 ] -> Commit
2. Anatomy of a Fiber Node
At its core, a Fiber is a JavaScript object representing a unit of component work. Instead of relying on the native JavaScript call stack frames, React stores call stack information in heap-allocated Fiber nodes.
Here is a simplified representation of the FiberNode structure used internally by React:
export interface FiberNode {
// Instance & Component Type
tag: WorkTag; // Type of work (FunctionComponent, ClassComponent, HostComponent)
key: null | string; // Unique key prop
elementType: any; // React element type
type: any; // Component function or DOM tag ("div", "button")
stateNode: any; // Reference to real DOM node instance or class instance
// Fiber Tree Structure (Singly Linked List)
return: FiberNode | null; // Parent Fiber node
child: FiberNode | null; // First child Fiber node
sibling: FiberNode | null; // Immediate sibling Fiber node
// State & Hook Queues
memoizedState: any; // Processed hook state list
updateQueue: any; // Pending state updates queue
// Double Buffering & Effects
alternate: FiberNode | null; // Pointer to current <-> workInProgress node
flags: Flags; // Bitmask of side effects (Placement, Update, Deletion)
lanes: Lanes; // Priority lanes bitmask
}
The Linked List Structure
Unlike traditional trees with children: FiberNode[] arrays, Fiber uses a singly-linked list traversal model:
child: Points to the component's first child.sibling: Points to the immediate sibling on the same level.return: Points back to the parent Fiber node (representing return address on stack).
This architecture allows React to pause rendering at any arbitrary node, record the current pointer, yield to the browser, and resume traversal from that exact node later.
3. Double Buffering: Current vs. WorkInProgress Trees
React Fiber implements a graphics technique known as Double Buffering to prevent incomplete renders from being rendered to the DOM:
- Current Tree: Represents the UI currently rendered on the browser screen.
- WorkInProgress (WIP) Tree: A draft tree built concurrently in memory during reconciliation.
+------------------+ +-------------------------+
| Current Tree | <--alternate--> | WorkInProgress Tree |
| (On Screen DOM) | | (Reconciling Draft) |
+------------------+ +-------------------------+
When state changes:
- React creates or reuses nodes for the
workInProgresstree by copying properties from thecurrenttree via thealternatepointer. - React computes diffs and assigns effect flags (
Placement,Update,Deletion) to the WIP nodes. - When reconciliation finishes, React points the root fiber to the completed WIP tree, making it the new
currenttree in a single atomic pointer swap!
4. The Work Loop & Time Slicing
The engine driving Fiber execution is the Work Loop. React schedules work using the Scheduler package.
Simplified Work Loop Implementation
let nextUnitOfWork: FiberNode | null = null;
let workInProgressRoot: FiberNode | null = null;
function workLoopConcurrent() {
// Perform work until there are no remaining units of work OR we run out of time in frame
while (nextUnitOfWork !== null && !shouldYieldToHost()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
function performUnitOfWork(unitOfWork: FiberNode): FiberNode | null {
const current = unitOfWork.alternate;
// 1. Begin work on current fiber (run component function, compute hooks, return children)
let next = beginWork(current, unitOfWork);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
// 2. If children exist, process the first child next
if (next !== null) {
return next;
}
// 3. No child left: complete work for this node and move to sibling or parent
let node: FiberNode | null = unitOfWork;
while (node !== null) {
completeWork(current, node);
if (node.sibling !== null) {
return node.sibling;
}
node = node.return;
}
return null;
}
If shouldYieldToHost() returns true (indicating the browser's 5ms frame deadline is approaching), React pauses execution, yields control to allow browser painting/user input, and schedules the next slice of work via postMessage or requestIdleCallback.
5. The Two Execution Phases
React splits every update cycle into two distinct operational phases:
Phase 1: Render / Reconciliation (Asynchronous & Interruptible)
- Traverses: Fiber tree from root down.
- Executes: Component functions, Hook evaluations, calculation of updated state.
- Side-Effects: Zero DOM mutations.
- Interruptible: Can be paused, restarted, or discarded if a higher-priority update arrives.
Phase 2: Commit Phase (Synchronous & Uninterruptible)
Once reconciliation finishes:
- React switches to the Commit Phase.
- Mutates the DOM in one synchronous pass (applying
Placement,Update,Deletionflags). - Runs
useLayoutEffectsynchronously before browser paint. - Swaps the
currentroot pointer to theworkInProgresstree. - Schedules
useEffectcleanup and callback hooks asynchronously.
6. Priority Lanes & Concurrent Features
In modern React (v18+), Fiber organizes updates into discrete Lanes (using 32-bit integer bitmasks). This enables fine-grained prioritization:
| Priority Lane | Trigger / Example | Interruptible? |
|---|---|---|
| SyncLane | User discrete clicks, keypresses, input change | No |
| InputContinuousLane | Mousemove, scrolling, drag events | Yes |
| DefaultLane | Data fetches, network responses, timers | Yes |
| TransitionLane | startTransition, useDeferredValue | Yes (Lowest) |
Practical Example: useTransition
import React, { useState, useTransition } from 'react';
export function SearchDashboard() {
const [query, setQuery] = useState('');
const [list, setList] = useState<string[]>([]);
const [isPending, startTransition] = useTransition();
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
// High Priority Update: Immediate input response
setQuery(e.target.value);
// Low Priority Update: Fiber marks list filtering under TransitionLane
startTransition(() => {
const filtered = heavyFilterAlgorithm(e.target.value);
setList(filtered);
});
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <p>Filtering records...</p>}
<ResultsList items={list} />
</div>
);
}
Because startTransition marks Fiber work with TransitionLane, user input in <input /> will instantly interrupt background list rendering, preserving 60 FPS responsiveness!
Conclusion & Architectural Key Takeaways
- Heap-Allocated Stack: Fiber replaces recursive call stacks with singly-linked list nodes (
child,sibling,return) stored in memory. - Double Buffering: Changes are rendered to an offline
workInProgresstree and committed atomically. - Time-Slicing: Work is broken into 5ms slices, giving high-priority user events immediate CPU access.
- Lane-Based Priority: Enables features like
useTransitionto decouple urgent UI feedback from expensive background renders.