Fiber Architecture
The Problem with the Stack Reconciler
Ever wondered why React needed a complete architectural rewrite? Before React 16, reconciliation was recursive. When setState fired, React walked the entire component tree synchronously — calling render on every component, diffing the result, and applying DOM updates. All of it happened in a single, uninterruptible call stack.
// Pre-Fiber: recursive, synchronous, uninterruptible
function reconcile(element, container) {
// Compare old and new trees
// Recursively process every child
// Apply all DOM mutations
// ALL of this blocks the main thread
}
On a complex app with 10,000 components, a single setState could lock the main thread for 100ms+. That means dropped frames, frozen inputs, and janky animations. The browser cannot process user input, run animations, or paint until JavaScript yields. And here's the core problem: a recursive call stack cannot yield — it either runs to completion or blows up.
The stack reconciler is like a surgeon who must complete an entire operation in one go — no breaks, no pauses, no matter how long it takes. Fiber is like breaking the operation into discrete steps that can be paused and resumed. Between steps, the surgeon can check if there is a more urgent patient (user input) and handle that first.
The Fiber Node
So what did the React team replace the stack with? A Fiber is a plain JavaScript object that represents a unit of work. Every React element — every component, every DOM node, every fragment — gets a corresponding Fiber node. Together, these nodes form a tree, but unlike a traditional tree with children arrays, Fiber uses a linked-list structure.
interface FiberNode {
// Identity
tag: WorkTag; // FunctionComponent, ClassComponent, HostComponent, etc.
type: any; // The component function/class, or 'div', 'span', etc.
key: string | null; // Reconciliation key
// Tree structure (linked list)
child: FiberNode | null; // First child
sibling: FiberNode | null; // Next sibling
return: FiberNode | null; // Parent
// Props & State
pendingProps: any; // Props for the current render
memoizedProps: any; // Props from the last completed render
memoizedState: any; // State from the last completed render
updateQueue: any; // Queue of pending state updates
// Effects
flags: Flags; // What side effects this fiber has (Placement, Update, Deletion)
subtreeFlags: Flags; // Aggregated flags from children
// Double buffering
alternate: FiberNode | null; // The "other" fiber (current ↔ workInProgress)
// Scheduling
lanes: Lanes; // Priority of pending work
childLanes: Lanes; // Priority of pending work in subtree
}
The Linked-List Tree
Consider this component tree:
<App>
<Header />
<Main>
<Sidebar />
<Content />
</Main>
</App>
In Fiber, this becomes:
App
│
child
↓
Header ──sibling──→ Main
│
child
↓
Sidebar ──sibling──→ Content
Every node points down to its first child, right to its sibling, and up to its return (parent). This structure lets React traverse the entire tree using a simple loop — no recursion needed.
// Simplified traversal — this is the actual pattern React uses
let fiber = rootFiber;
while (fiber !== null) {
// Process this fiber
performWork(fiber);
// Go to child first
if (fiber.child) {
fiber = fiber.child;
continue;
}
// No child — try sibling
while (fiber !== null) {
// Going back up — complete work for this fiber
completeWork(fiber);
if (fiber.sibling) {
fiber = fiber.sibling;
break; // Process sibling in next iteration
}
fiber = fiber.return; // Go up to parent
}
}
The Work Loop
Here's where it gets interesting. React's rendering happens in a loop called the work loop. It processes one Fiber at a time, and between each unit of work, it can check whether the browser needs the main thread back.
// Simplified from React source (ReactFiberWorkLoop.js)
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
Take a close look at the difference between these two functions. The only thing separating sync and concurrent mode is one function call: shouldYield(). In sync mode, React processes everything without stopping. In concurrent mode, React checks after each fiber whether 5ms have elapsed, and if so, yields to the browser.
performUnitOfWork
Each unit of work has two phases:
function performUnitOfWork(unitOfWork) {
const next = beginWork(unitOfWork); // Phase 1: Process this fiber
if (next !== null) {
workInProgress = next; // Has child — process child next
} else {
completeUnitOfWork(unitOfWork); // Phase 2: No child — complete and move to sibling/parent
}
}
beginWork
beginWork is where the actual component logic runs. Depending on the fiber's tag, it:
- FunctionComponent: Calls the function, processes hooks
- ClassComponent: Calls
render(), processes lifecycle - HostComponent (
div,span): Reconciles children - Returns the fiber's child (or
nullif it is a leaf node)
completeWork
completeWork runs when a fiber has no more children to process. It:
- Creates DOM nodes for host components (but does not insert them yet)
- Bubbles up effect flags from children to parents via
subtreeFlags - Builds the side effect information that the commit phase will use
Double Buffering: Current vs WorkInProgress
This is one of those tricks that sounds obvious once you hear it, but it's genuinely clever. React maintains two fiber trees at all times:
- current: The tree that is currently rendered to the screen
- workInProgress: The tree being built for the next render
Each fiber in current has an alternate pointing to its counterpart in workInProgress, and vice versa. When React begins a render, it clones fibers from current into workInProgress, applying updates as it goes.
current tree workInProgress tree
App ◄──alternate──► App (updated)
│ │
Header ◄──────────► Header (reused)
│ │
Main ◄──────────► Main (updated)
When the render completes and the commit phase finishes, React swaps the pointers: workInProgress becomes current. The old current tree becomes the base for the next workInProgress. This avoids allocating a fresh tree every render — fibers are recycled.
Priority Lanes
Not all updates are equal — and this is the part that makes concurrent React truly powerful. A user typing in an input needs immediate feedback. A data fetch populating a list can wait. React's lane system assigns priorities to updates.
// Lane priorities (simplified from ReactFiberLane.js)
SyncLane = 0b0000000000000000000000000000001; // Highest: discrete user input
InputContinuousLane = 0b0000000000000000000000000000100; // Continuous input (drag, scroll)
DefaultLane = 0b0000000000000000000000000010000; // Normal: setState, fetch results
TransitionLane = 0b0000000000000000001000000000000; // startTransition
IdleLane = 0b0100000000000000000000000000000; // Lowest: offscreen, prefetch
Lanes are bitmasks. React can merge multiple lanes with bitwise OR, check if a fiber has work at a specific priority with bitwise AND, and quickly determine the highest-priority pending work.
// Does this fiber have sync work?
if (fiber.lanes & SyncLane) {
// Process immediately
}
// Does the subtree have any transition work?
if (fiber.childLanes & TransitionLanes) {
// There's transition work somewhere below
}
When a higher-priority update arrives while React is in the middle of rendering a lower-priority update, React can abandon the in-progress workInProgress tree and start over with the higher-priority work. The interrupted work is not lost — it will be attempted again after the urgent work commits.
Why This Architecture Matters
Let's zoom out. Fiber is not an optimization. It is a new rendering architecture that enables capabilities impossible with the stack reconciler:
- Interruptible rendering — React can pause rendering to handle urgent work, then resume where it left off
- Concurrent features —
useTransition,useDeferredValue,Suspenseall depend on Fiber's ability to prepare multiple versions of the UI simultaneously - Priority scheduling — Different updates run at different priorities, keeping the UI responsive
- Incremental rendering — Large trees can be rendered across multiple frames instead of blocking for hundreds of milliseconds
Fiber does NOT make rendering faster in raw throughput. Processing 10,000 components still takes the same total CPU time. What Fiber does is spread that work across frames so the user never perceives a freeze. A 100ms render in the stack reconciler becomes twenty 5ms chunks in Fiber — same total work, but the browser can paint and handle input between chunks. Perceived performance improves dramatically even though measured total render time may be slightly higher due to scheduling overhead.
- 1A Fiber is a JavaScript object representing one unit of work. Every React element has a corresponding Fiber node.
- 2Fibers form a linked list via child, sibling, and return pointers — enabling pausable traversal without recursion.
- 3The work loop processes one fiber at a time. In concurrent mode, it yields every ~5ms via shouldYield().
- 4beginWork processes a fiber (calls the component, reconciles children). completeWork bubbles effects upward and creates DOM nodes.
- 5React double-buffers with current and workInProgress trees. After commit, workInProgress becomes current.
- 6Priority lanes are bitmasks. Higher-priority work (SyncLane) can interrupt lower-priority work (TransitionLane).
- 7Fiber does not reduce total render time — it distributes work across frames to keep the UI responsive.
Q: Explain how React Fiber enables interruptible rendering. What data structure changes were necessary, and how does the work loop decide when to yield?
A strong answer covers: the shift from recursive call stack to iterative linked-list traversal (child/sibling/return pointers), the work loop checking shouldYield() after each fiber unit, the double buffering of current and workInProgress trees, how interrupted work is discarded and restarted when higher-priority updates arrive (via the lane system), and how beginWork/completeWork decompose rendering into discrete, resumable steps.