For many developers, React operates as a highly reliable black box. You trigger a state mutation and the UI eventually reflects that change.
However, when you are architecting large-scale applications with high-frequency updates, complex animations or heavy data visualization, treating React as a black box is a liability.
To resolve frame drops, optimize render cycles, and leverage Concurrent React properly, you must understand the underlying engine: React Fiber.
This article explores the architectural shift from React’s legacy Stack Reconciler to the Fiber architecture, examining the underlying data structures, cooperative scheduling and the mechanics of the render and commit phases.
The Bottleneck: The Legacy Stack Reconciler
To understand why Fiber exists, it helps to analyze what React was doing before version 16 and where that model broke down.
The original reconciliation algorithm, known as the Stack Reconciler, relied on a synchronous, recursive traversal of the Virtual DOM tree. When setState was called, React would push the update onto the JavaScript call stack and recursively walk the entire component tree, parent to child, depth-first, computing the differences between the previous and next virtual DOM snapshots. This process is called reconciliation or the diffing phase.
The architectural flaw was not in the diffing algorithm itself. React’s O(n) heuristic-based diffing was and still is, efficient for most trees. The flaw was in the execution model.
This traversal was strictly synchronous and atomic. Once React began reconciling a tree, it held an exclusive lock on the JavaScript main thread until the entire tree was processed. The call stack had to unwind completely before control was returned to the browser. If a deeply nested component tree took 80–150 milliseconds to reconcile, an easy threshold to cross in data-heavy dashboards or large list renders, the browser could not execute anything else during that window.
The consequences were concrete and measurable:
Input latency — Keystrokes and click events queued in the browser’s event queue, but were not processed until React released the main thread. Users experienced sluggish, unresponsive inputs.
Animation jank — CSS animations and requestAnimationFrame callbacks missed their 16ms frame budget, the threshold for a smooth 60fps experience, causing visible stuttering.
Scroll blocking — Scroll event handlers were starved, producing a frozen or choppy scrolling experience.
The root of the problem is architectural: the native JavaScript call stack cannot be paused, reprioritized or partially rewound. Once a function is executing, it runs to completion. React had no mechanism to say, “pause here, let the browser paint a frame, then resume.”
React needed four capabilities the Stack Reconciler fundamentally could not provide:
Pause work and resume it later.
Abort work that is no longer relevant, such as a state update superseded by a newer one.
Reuse previously completed work.
Prioritize different types of updates differently.
The solution was radical: move the stack from the native call stack to the heap, and build a custom scheduler on top of it.
The Paradigm Shift: Fiber as a Virtual Stack Frame
React Fiber is a complete rewrite of React’s core reconciliation and scheduling engine, shipped in React 16 in September 2017, with the full Concurrent Mode capabilities unlocked progressively through React 18.
The overarching goal of Fiber is to enable cooperative scheduling and time-slicing, the ability for React to voluntarily yield execution back to the browser between units of work.

From Recursion to a Linked List
The Stack Reconciler traversed the component tree using JavaScript’s native recursion. Each function call pushed a new frame onto the call stack. This made the traversal inherently synchronous: the only way to pause recursion is to not start it, which is not useful in practice.
Fiber solves this by virtualizing the call stack. Instead of relying on the engine’s call stack, React allocates a custom data structure on the heap for each component in the tree. This data structure is called a Fiber node.
A Fiber node is a plain JavaScript object. It is React’s unit of work, a heap-allocated representation of a component instance, equivalent to a single frame in the call stack that React controls entirely. Because these objects live on the heap rather than the call stack, React can:
Stop processing at any Fiber node.
Store the current Fiber node’s reference.
Hand control back to the browser’s event loop.
Resume from the exact same Fiber node in the next available time slice.
This is cooperative scheduling: React voluntarily yields rather than being forcibly interrupted.
The Anatomy of a Fiber Node
Understanding what a Fiber node contains is key to understanding how React tracks, schedules and executes work.
Each Fiber node holds 
The return, child, and sibling pointers are critical. Rather than a traditional tree structure, Fiber represents the component tree as a singly-linked list traversal graph. This structure is what allows React to traverse iteratively in a while loop rather than recursively, making it interruptible at any node boundary.
The Double Buffer: Current and Work-In-Progress Trees
At any given moment, React maintains two fiber trees in memory simultaneously:
Current tree — The fiber tree that corresponds to what is currently rendered and visible on screen. This tree is stable and never mutated directly.
Work-in-progress tree — A new tree React is constructing in the background to reflect the next UI state. This is where all reconciliation and diffing occurs.
Each fiber node in the work-in-progress tree has an alternate pointer back to its counterpart in the current tree, and vice versa. React reuses fiber objects between renders, updating their fields rather than creating new objects, to minimize garbage collection pressure.
When the work-in-progress tree is complete and all mutations are committed to the DOM, React performs an atomic tree swap: the work-in-progress tree becomes the new current tree. This pattern is called double buffering, the same technique used in graphics programming to prevent screen tearing.
The critical architectural benefit is that if work on the work-in-progress tree is interrupted or abandoned due to a higher-priority update arriving, React can simply discard the in-progress tree. The current tree, representing the last consistent UI state, remains untouched and valid.
The Work Loop: Cooperative Scheduling in Practice
At the heart of Fiber’s execution model is the work loop, a while loop that processes one fiber node at a time and checks, between each unit, whether it should yield to the browser.
function workLoopConcurrent()
{
while (workInProgress !== null && !shouldYield())
{
performUnitOfWork(workInProgress);
}
}
The shouldYield() function is provided by React’s Scheduler package, a standalone, platform-agnostic priority scheduling library. The Scheduler uses a deadline-based model: React requests a time slice, nominally 5ms in the current implementation, processes as many fiber nodes as possible within that slice and then yields if the deadline is exceeded.
Why MessageChannel and Not setTimeout
React’s Scheduler yields control using MessageChannel rather than setTimeout(fn, 0). This is a deliberate and important choice:
setTimeout has a minimum delay of 4ms enforced by browsers after nested calls, which wastes precious frame budget.
MessageChannel posts a macrotask that executes after the browser has processed any pending rendering and input events, but before the next setTimeout fires, making it significantly lower-latency and higher-throughput for a scheduling loop.
This allows React’s work loop to resume as quickly as possible after yielding, maximizing the work done within each frame’s idle time.
The Two Phases of a Render Cycle
Fiber divides every render cycle into two fundamentally distinct phases with different guarantees and characteristics.
Phase 1 — The Render Phase
The render phase is where React determines what changed. During this phase, React traverses the work-in-progress tree using the work loop described above. For each fiber node, React calls the component function or render() method for class components, processes hooks, runs useMemo and useCallback comparisons, and diffs the returned React element tree against the previous fiber’s memoizedProps and memoizedState.
The output of the render phase is a set of effect flags on each fiber, bitmask values indicating what DOM operations are needed, such as Placement, Update, Deletion, PassiveEffect, and LayoutEffect.
Critical characteristics of the render phase:
It is pure and free of observable side effects. React may call your component function multiple times.
It is interruptible. If a higher-priority update arrives, React can abort the current work-in-progress tree, discard it entirely and restart with the new update incorporated.
It is asynchronous in Concurrent Mode. React does not need to complete the render phase in a single synchronous pass.
Phase 2 — The Commit Phase
Once the render phase completes and the work-in-progress tree is fully built, React enters the commit phase. This is where actual DOM mutations occur: inserting nodes, updating attributes, removing elements and calling refs.
The commit phase is divided internally into three sub-phases, executed in strict order:
Before mutation sub-phase — React reads the DOM state before any mutations, used for getSnapshotBeforeUpdate in class components and schedules useEffect cleanups and callbacks asynchronously.
Mutation sub-phase — React applies all DOM mutations, insertions, updates and deletions. useLayoutEffect cleanup functions from the previous render are called here.
Layout sub-phase — useLayoutEffect setup callbacks are called synchronously after DOM mutations but before the browser has painted. componentDidMount and componentDidUpdate are called here for class components. The current pointer is swapped, and the work-in-progress tree officially becomes the current tree at this point.
After the browser paints, React asynchronously flushes all pending useEffect callbacks, both cleanup and setup, using the Scheduler to run them at idle priority.
The commit phase must be synchronous because the DOM must always be in a consistent state. A half-applied set of DOM mutations would present a broken, visually inconsistent UI to the user. Unlike the render phase, where restarting is safe because it produces no observable side effects, partially committing DOM mutations is not recoverable. Therefore, once the commit phase begins, it runs to completion without interruption.
Priority Scheduling: The Lanes Model
Fiber’s scheduler assigns every update a priority level. As of React 18, this is implemented via the Lanes model, a bitmask-based priority system that replaced the earlier ExpirationTime model.
A lane is a bit in a 31-bit integer. React can batch, group, and compare lanes using standard bitwise operations, which is extremely fast. The lane assignment determines
when React processes an update relative to others.
Lane | Triggered by | Priority |
SyncLane | flushSync(), controlled inputs | Highest — synchronous, never batched |
InputContinuousLane | onDrag, onScroll | Very high |
DefaultLane | Standard setState, useReducer | Normal |
TransitionLane (1–15) | startTransition() | Low — interruptible |
RetryLane | Suspense retries | Low |
IdleLane | Background prefetch/prerender | Lowest |
When a new update arrives, React performs a lane intersection check. It compares the incoming update’s lane against the lanes of work currently in progress. If a higher-priority lane arrives while a lower-priority render is underway, React interrupts the lower-priority work-in-progress tree, incorporates the high-priority update, processes it first and then, if the previous work is still relevant, schedules the lower-priority work to resume.
startTransition and useDeferredValue
const handleChange = (e) =>
{
setQuery(e.target.value);
startTransition(() =>
{
setFilteredResults(computeExpensiveFilter(e.target.value));
});
};
startTransition does not make the computation faster. It tells React’s scheduler to process the input state update first and interrupt the filter computation if needed. The filter computation will eventually complete, just without blocking user interaction.
useDeferredValue achieves a similar effect, but at the value-consumption level rather than the update-dispatch level. This is useful when the state-setting code is not directly under your control.
Reconciliation: How React Decides What Changed
Within each fiber’s render-phase processing, React runs its diffing algorithm to compare the previous and next elements. React’s reconciliation heuristics are deliberately O(n), rather than the theoretically optimal O(n³) tree diff.
– Different type — Tear down the entire subtree and build fresh. React never attempts to reconcile a `<div>` into a `<span>`.
Same type, different props — Update the existing fiber node in place, setting flags such as Update.
Lists with key prop — React uses keys to match children across renders. Without keys, React uses positional index matching, which produces incorrect results when list items are reordered.
React.memo and PureComponent — If props are shallowly equal, React bails out. It marks the fiber as having no work and skips the entire subtree, short-circuiting the work loop for that branch.
This bailout mechanism is powerful. A well-structured component tree with appropriate memoization can result in React only reconciling a tiny fraction of the full tree on any given update.
Practical Implications for Senior Developers
Understanding Fiber translates directly into better architectural decisions:
Frame drops despite memoization — Check whether expensive computations are in DefaultLane. Move non-urgent state updates into startTransition.
useLayoutEffect causing paint delays — It runs synchronously in the commit phase before the browser paints. Move any non-DOM-measurement logic to useEffect.
Concurrent Mode subtleties — Component functions may be called multiple times per committed render. Any side effects inside the render body, rather than in useEffect, will fire multiple times.
Large list performance — Virtualization reduces the fiber tree size, directly reducing the number of units of work in the work loop.
Suspense boundaries — Suspense is a first-class Fiber construct. A suspended fiber causes React to walk up the tree to the nearest <Suspense> boundary and render the fallback, while keeping the suspended subtree’s fiber nodes allocated so React can resume them when the promise resolves.
Conclusion
React Fiber is not merely an implementation detail. It is the architectural foundation that makes modern React possible. The shift from a synchronous, recursive Stack Reconciler to a heap-allocated, cooperative, priority-driven Fiber architecture fundamentally changed what React can do.
By internalizing the double-buffering model, the two-phase render/commit split, the Lanes priority system and the work loop’s cooperative yielding, you gain the mental model to diagnose performance issues at their root cause, use Concurrent React APIs with precision and design component architectures that work with the scheduler rather than against it.
The developers who understand Fiber do not just fix jank. They design systems where jank cannot occur.


