Prior to version 16, the React core relied on a recursive virtual tree diffing engine known as the Stack Reconciler. Whenever state updated, the engine traversed the entire component tree from top to bottom synchronously on the native JavaScript Call Stack.
In component trees containing thousands of nodes, this operation blocked the browser's main thread for 50–200ms. During this blocking window, the browser was completely unable to respond to user input, update CSS transitions, or render frames, causing severe visual stutter and Frame Drops.
The React Fiber architecture overhauled this paradigm from first principles. Instead of relying on the engine's implicit call stack, Fiber implements a heap-allocated virtual stack frame, where every tree node is modeled as an explicit FiberNode data structure. This architectural shift enabled:
Unlike traditional DOM or vDOM trees that rely on child arrays (children: [...]), the internal Fiber graph is organized as a singly linked list structure: each node points exclusively to its first child (child), its next sibling (sibling), and its parent node (return).
[ Current Tree (Mounted in DOM) ] [ WorkInProgress Tree (Built in background) ]
FiberRoot FiberRoot
│ │
(current pointer) (workInProgress)
▼ ▼
[FiberNode: App] <====== alternate ======> [FiberNode: App]
/ ▲ / ▲
child/ \return child/ \return
▼ \ ▼ \
[FiberNode: Header] \ [FiberNode: Header] \
│ \ │ \
sibling\ \ sibling\ \
▼ \ ▼ \
[FiberNode: Main] ─────┘ [FiberNode: Main] ─────┘This linked structure yields a fundamental engineering advantage: the graph is traversed iteratively inside a flat while loop without Call Stack recursion. At any point, the runtime can preserve a reference to the active node, yield the thread back to the browser engine for layout and painting, and resume computation from the exact checkpoint.
The React core maintains two synchronized trees in heap memory:
workInProgress tree in a single atomic O(1) assignment: FiberRoot.current = workInProgress.Double buffering guarantees that end users never witness partial, intermediate UI states. If rendering an expensive subtree is aborted by an unhandled error or preempted by urgent keystrokes, the incomplete workInProgress tree is discarded without compromising the active display.
A user interaction triggers an update. The scheduler calculates a bitmask priority (e.g. SyncLane or TransitionLane) and marks the path to the root via childLanes.
The workInProgress root node is retrieved or instantiated via its alternate pointer, mirroring props from the active current tree.
Iterate downward. Execute component functions, compute state updates, evaluate bailouts (memoization), and reconcile child JSX into new FiberNodes.
Instantiate and configure host DOM nodes in memory, attach event listeners, calculate layout properties, and bubble up subtreeFlags bitmasks.
Synchronously apply mutations to the live DOM: invoke useLayoutEffect cleanups, mutate host elements, swap FiberRoot.current atomically, and queue passive useEffects.
Why did the React core discard classical child-array trees (children: [...]) in favor of singly linked list pointers (child, sibling, return)?
Rendering in React is strictly bifurcated into two conceptual phases: the Render Phase (asynchronous, interruptible, free of side effects) and the Commit Phase (synchronous, uninterruptible, mutating host DOM nodes).
Computes event bitmask (e.g. SyncLane for keystrokes or TransitionLane for searches) and propagates childLanes from the target node up to FiberRoot.
Processes discrete performUnitOfWork steps. Descends via beginWork and ascends via completeWork while checking shouldYield() budgets.
Compares current FiberNode against fresh JSX elements. Applies mutation effect flags (Placement, Update, ChildDeletion).
Applies mutation effects synchronously: executes useLayoutEffect destructors, updates host DOM, swaps current pointer, schedules useEffect.
Every FiberNode is allocated by the core runtime. Below is the strict TypeScript interface representing its fundamental internal fields in React 19:
export type WorkTag =
| 0 // FunctionComponent
| 1 // ClassComponent
| 2 // IndeterminateComponent
| 3 // HostRoot (Root node)
| 5 // HostComponent (<div>, <span>)
| 6 // HostText
| 11 // ForwardRef
| 13 // SuspenseComponent
| 24; // CacheComponent
export type Flags = number; // Mutation bitmask (Placement, Update, Deletion)
export type Lane = number; // 31-bit priority bitmask
export type Lanes = number;
export interface FiberNode {
// Identity & Element Descriptors
tag: WorkTag;
key: null | string;
elementType: unknown;
type: unknown;
stateNode: unknown; // Pointer to host DOM node or class component instance
// Graph Traversal Pointers
return: FiberNode | null; // Parent node (ascent target)
child: FiberNode | null; // First child node
sibling: FiberNode | null; // Next sibling node
index: number;
// Input & Computed State
pendingProps: Record<string, unknown>; // Props awaiting execution
memoizedProps: Record<string, unknown>; // Props rendered in previous commit
memoizedState: unknown; // Linked list of React Hooks
updateQueue: unknown; // Queue of pending state transitions
// Flags & Priority Lanes
flags: Flags; // Local mutation side effects on this node
subtreeFlags: Flags; // Aggregated mutation side effects in descendant tree
deletions: Array<FiberNode> | null; // Child nodes queued for removal
lanes: Lanes; // Pending priority lanes for this node
childLanes: Lanes; // Pending priority lanes in descendant subtree
// Double Buffering Mechanism
alternate: FiberNode | null; // Dual pointer between current and workInProgress
}Render phase execution is coordinated by the workLoopConcurrent function. The shouldYield() check consults the Scheduler, which allocates 5ms execution budgets:
import { shouldYield } from './schedulerHostConfig';
let workInProgress: FiberNode | null = null;
let renderLanes: Lanes = 0;
export function workLoopConcurrent(): void {
// Execute units of work cooperatively until work is complete or time budget expires
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork: FiberNode): void {
const current = unitOfWork.alternate;
// 1. Descent phase: evaluate component function, reconcile child JSX
let next: FiberNode | null = beginWork(current, unitOfWork, renderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 2. If no children exist, initiate the ascent phase
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
function completeUnitOfWork(unitOfWork: FiberNode): void {
let completedWork: FiberNode | null = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber: FiberNode | null = completedWork.return;
// Assemble host properties, bubble up subtreeFlags
completeWork(current, completedWork, renderLanes);
const siblingFiber: FiberNode | null = completedWork.sibling;
if (siblingFiber !== null) {
// If a sibling exists, shift focus to descend down the sibling
workInProgress = siblingFiber;
return;
}
// Ascend to parent
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
}Unlike the legacy expirationTime paradigm, which modeled priority as scalar integers (introducing head-of-line blocking across unrelated updates), Lanes represent priority as distinct bit channels. A 32-bit integer provides 31 independent priority lanes:
export const TotalLanes = 31;
export const NoLanes: Lanes = 0b0000000000000000000000000000000;
export const NoLane: Lane = 0b0000000000000000000000000000000;
// Synchronous blocking updates (discrete user input: clicks, typing)
export const SyncLane: Lane = 0b0000000000000000000000000000001;
// Continuous user input (scrolling, mouse movement)
export const InputContinuousLane: Lane = 0b0000000000000000000000000000010;
// Standard asynchronous updates (state updates, network responses)
export const DefaultLane: Lane = 0b0000000000000000000000000010000;
// Transitions (startTransition, useDeferredValue)
export const TransitionLanes: Lanes = 0b0000000011111111111111100000000;
export const TransitionLane1: Lane = 0b0000000000000000000000100000000;
// Idle background tasks
export const IdleLanes: Lanes = 0b0110000000000000000000000000000;
/**
* Extract highest priority lane (rightmost active bit)
* Bitwise idiom: x & -x returns the least significant set bit
*/
export function getHighestPriorityLane(lanes: Lanes): Lane {
return lanes & -lanes;
}
/**
* Check if a lane set contains a subset
*/
export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane): boolean {
return (set & subset) === subset;
}
/**
* Combine lane bitmasks
*/
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a | b;
}
/**
* Remove processed lanes from active set
*/
export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
return set & ~subset;
}The expression lanes & -lanes utilizes Two's Complement representation of signed integers. In hardware, -lanes equals ~lanes + 1. The bitwise AND clears all bits except the Least Significant Bit in a single CPU cycle, enabling the scheduler to resolve the highest-priority Lane instantaneously.
React resolves the challenge of diffing two child lists in linear O(N) time rather than standard O(N^3) graph editing distance using a two-pass heuristic:
key identifiers match. Upon the first key mismatch, the loop terminates.Map<Key | Index, FiberNode>. New elements are queried against this map in O(1) time. Matched nodes are reused with a Placement flag, and unmatched old nodes are tagged with ChildDeletion.interface ChildElement {
key: string | null;
type: unknown;
props: Record<string, unknown>;
}
export function reconcileChildrenArray(
returnFiber: FiberNode,
currentFirstChild: FiberNode | null,
newChildren: ChildElement[],
lanes: Lanes
): FiberNode | null {
let resultingFirstChild: FiberNode | null = null;
let previousNewFiber: FiberNode | null = null;
let oldFiber = currentFirstChild;
let nextOldFiber: FiberNode | null = null;
let lastPlacedIndex = 0;
let newIdx = 0;
// PASS 1: Iterate sequentially while keys match
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newChild = newChildren[newIdx];
if (oldFiber.key !== newChild.key) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
const matchedFiber = updateSlot(returnFiber, oldFiber, newChild, lanes);
if (matchedFiber === null) {
break;
}
lastPlacedIndex = placeChild(matchedFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = matchedFiber;
} else {
previousNewFiber.sibling = matchedFiber;
}
previousNewFiber = matchedFiber;
oldFiber = nextOldFiber;
}
// If new elements are exhausted, remove remaining old nodes
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// PASS 2: Place remaining old nodes in Map for O(1) matching
if (oldFiber !== null) {
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
const newChild = newChildren[newIdx];
const existingFiber = existingChildren.get(
newChild.key !== null ? newChild.key : newIdx
) || null;
const newFiber = updateFromMap(existingFiber, returnFiber, newChild, lanes);
if (newFiber !== null) {
if (existingFiber !== null) {
existingChildren.delete(newChild.key !== null ? newChild.key : newIdx);
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
// Remaining nodes in the map are marked for deletion
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
function placeChild(newFiber: FiberNode, lastPlacedIndex: number, newIndex: number): number {
newFiber.index = newIndex;
const current = newFiber.alternate;
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// Element moved right relative to its original sibling order
newFiber.flags |= 0b0000000000000000000000000000010; // Placement
return lastPlacedIndex;
}
return oldIndex;
}
// Newly inserted node
newFiber.flags |= 0b0000000000000000000000000000010;
return lastPlacedIndex;
}
function updateSlot(
returnFiber: FiberNode,
oldFiber: FiberNode,
newChild: ChildElement,
lanes: Lanes
): FiberNode | null {
if (oldFiber.key === newChild.key && oldFiber.elementType === newChild.type) {
return useFiber(oldFiber, newChild.props);
}
return null;
}
function updateFromMap(
existingFiber: FiberNode | null,
returnFiber: FiberNode,
newChild: ChildElement,
lanes: Lanes
): FiberNode | null {
if (existingFiber !== null && existingFiber.elementType === newChild.type) {
return useFiber(existingFiber, newChild.props);
}
return createFiberFromElement(newChild, lanes);
}
function mapRemainingChildren(
returnFiber: FiberNode,
currentFirstChild: FiberNode
): Map<string | number, FiberNode> {
const existingChildren = new Map<string | number, FiberNode>();
let existing: FiberNode | null = currentFirstChild;
while (existing !== null) {
const key = existing.key !== null ? existing.key : existing.index;
existingChildren.set(key, existing);
existing = existing.sibling;
}
return existingChildren;
}
function useFiber(fiber: FiberNode, pendingProps: Record<string, unknown>): FiberNode {
const clone = fiber.alternate || { ...fiber };
clone.pendingProps = pendingProps;
return clone;
}
function createFiberFromElement(child: ChildElement, lanes: Lanes): FiberNode {
return {
tag: 0,
key: child.key,
elementType: child.type,
type: child.type,
stateNode: null,
return: null,
child: null,
sibling: null,
index: 0,
pendingProps: child.props,
memoizedProps: {},
memoizedState: null,
updateQueue: null,
flags: 0b0000000000000000000000000000010,
subtreeFlags: 0,
deletions: null,
lanes,
childLanes: 0,
alternate: null
};
}
function deleteChild(returnFiber: FiberNode, childToDelete: FiberNode): void {
returnFiber.flags |= 0b0000000000000000000000000010000; // ChildDeletion
if (returnFiber.deletions === null) {
returnFiber.deletions = [childToDelete];
} else {
returnFiber.deletions.push(childToDelete);
}
}
function deleteRemainingChildren(returnFiber: FiberNode, currentFirstChild: FiberNode | null): void {
let child: FiberNode | null = currentFirstChild;
while (child !== null) {
deleteChild(returnFiber, child);
child = child.sibling;
}
}What occurs during the second pass of reconcileChildrenArray when an array element shifts from index 5 to index 0, provided its key remains stable?
To inspect Fiber internals at runtime, this utility extracts active priority lanes and node statistics directly from live DOM elements:
interface FiberDebugSnapshot {
tag: number;
key: string | null;
typeName: string;
lanes: number;
lanesBinary: string;
hasPendingWork: boolean;
childCount: number;
}
const REACT_FIBER_PREFIX = '__reactFiber$';
export function getFiberFromDom(domElement: HTMLElement): FiberNode | null {
const fiberKey = Object.keys(domElement).find(key => key.startsWith(REACT_FIBER_PREFIX));
if (!fiberKey) {
return null;
}
return ((domElement as unknown) as Record<string, FiberNode>)[fiberKey] || null;
}
export function inspectFiberSubtree(fiber: FiberNode): FiberDebugSnapshot {
let childCount = 0;
let currentChild = fiber.child;
while (currentChild !== null) {
childCount++;
currentChild = currentChild.sibling;
}
const typeName =
typeof fiber.type === 'function'
? fiber.type.name || 'AnonymousFunction'
: typeof fiber.type === 'string'
? fiber.type
: 'Fragment/Special';
return {
tag: fiber.tag,
key: fiber.key,
typeName,
lanes: fiber.lanes,
lanesBinary: fiber.lanes.toString(2).padStart(31, '0'),
hasPendingWork: fiber.lanes !== 0 || fiber.childLanes !== 0,
childCount
};
}When sorting, filtering, or prepending items, using key={index} forces the reconciliation algorithm to associate unrelated entities in the first pass. Fiber reuses old FiberNode instances with mismatched memoizedState, producing input desynchronization bugs and redundant DOM mutations.
// ❌ WRONG: Misaligns internal hook state and loses element focus
{items.map((user, index) => (
<UserRow key={index} data={user} />
))}
// ✅ CORRECT: Use stable unique entity identifiers from the database
{items.map((user) => (
<UserRow key={user.id} data={user} />
))}Declaring a component inside a parent's render function creates a fresh elementType function reference on every render pass. During reconciliation, oldFiber.elementType === newChild.type evaluates to false. React destroys the existing Fiber subtree completely (unmounting and running all useEffect cleanups) and mounts a new one from scratch, discarding all state.
// ❌ WRONG: Instantiates a new type reference on every render, resetting DOM & state
export function OrderManager() {
const StatusBadge = ({ active }: { active: boolean }) => (
<span className={active ? 'green' : 'red'} />
);
return <StatusBadge active={true} />;
}
// ✅ CORRECT: Hoist components to module scope
const StatusBadge = ({ active }: { active: boolean }) => (
<span className={active ? 'green' : 'red'} />
);
export function OrderManager() {
return <StatusBadge active={true} />;
}Invoking flushSync bypasses the cooperative scheduler and forces an immediate synchronous execution of workLoopSync for pending updates, draining SyncLane. Overusing flushSync inside loops or event handlers blocks the main UI thread and triggers frame drops.
import { flushSync } from 'react-dom';
// ❌ WRONG: Forces a complete synchronous render pass on every step
function handleBatchUpdates(items: string[]) {
items.forEach(item => {
flushSync(() => {
setList(prev => [...prev, item]);
});
});
}
// ✅ CORRECT: Rely on React 19 automatic batching
function handleBatchUpdates(items: string[]) {
setList(prev => [...prev, ...items]);
}Because the Render Phase is interruptible and may restart multiple times before committing, directly mutating stateNode (DOM) or modifying external variables within the component body produces visual inconsistencies and memory leaks. Host DOM mutations must occur strictly within the Commit Phase (useEffect, useLayoutEffect).