
How React’s Fiber Architecture Actually Works: Reconciliation, Lanes, and the Render Loop
By Ghazi Khan | Aug 2, 2026 - 9 min read
Most React developers can explain reconciliation in one sentence: "React compares the old and new tree and updates only what changed." That sentence is true and almost useless. It doesn't tell you why a setState call inside a click handler feels instant while a large list re-render can still cause a dropped frame, why React can abandon a render halfway through, or why the key prop matters at all.
This post explains what actually happens between the moment you call setState and the moment pixels change on screen, in the order it actually happens.
Why React Needed Fiber in the First Place
Before React 16, reconciliation used what's now called the stack reconciler. It walked the component tree recursively, calling each component's render function synchronously, one after another, with no way to stop midway. If your tree was deep or your update touched thousands of nodes, that recursive call stack ran to completion or not at all. There was no yielding back to the browser.
That's a real problem, not a theoretical one. The browser has one thread for JavaScript execution, layout, and painting. If a render pass takes 300ms, the browser cannot process a keystroke, cannot update a scroll position, and cannot paint a frame during that window. The page appears frozen because it is frozen.
React 16 replaced this with the Fiber reconciler. The core idea: break rendering work into small units, and make each unit interruptible. Instead of one giant recursive call, React walks the tree as a loop over a linked data structure, checking after each unit of work whether it should yield control back to the browser. This is what makes concurrent features like useTransition and useDeferredValue possible.

What a Fiber Actually Is
A "fiber" is a plain JavaScript object, one per component instance or DOM element, that represents a unit of work. It's not the same thing as the React element you write in JSX. A React element is a lightweight, immutable description of what you want rendered, discarded after every render. A fiber is the opposite: a persistent, mutable object that React keeps around between renders to track state.
Each fiber node holds, among other things:
{
type: 'div', // the underlying component or DOM tag
key: null, // used for list diffing
stateNode: domNodeOrInstance,// the actual DOM node or class instance
child: fiberOrNull, // first child fiber
sibling: fiberOrNull, // next sibling fiber
return: fiberOrNull, // parent fiber
pendingProps: {...}, // incoming props for this render
memoizedProps: {...}, // props from the last completed render
memoizedState: {...}, // hooks state, linked list for function components
flags: 0, // side effects to apply: Placement, Update, Deletion
alternate: fiberOrNull, // pointer to the fiber from the previous render
}
That child / sibling / return structure turns the component tree into a linked list that can be traversed without recursion, using a plain loop with an explicit pointer. That's the mechanism that makes pausable rendering possible: React just stops updating the pointer and remembers where it left off.

The alternate field is the other key piece. React actually keeps two fiber trees in memory: the "current" tree, which reflects what's on screen, and the "work-in-progress" tree, which React builds during a render. Each fiber's alternate points to its counterpart in the other tree. When a render commits, React simply flips a pointer so the work-in-progress tree becomes the current tree. This is called double buffering, the same technique used in graphics rendering to avoid showing a half-drawn frame.

The Render Phase: Building the Work-in-Progress Tree
When you call setState, React doesn't update the DOM immediately. It schedules an update on the relevant fiber and, eventually, starts the render phase.
During the render phase, React walks the tree fiber by fiber, in a loop that looks roughly like this internally:
function workLoop(deadline) {
while (nextUnitOfWork && deadline.timeRemaining() > 1) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (nextUnitOfWork) {
requestIdleCallback(workLoop); // yield, resume later
} else {
commitRoot(); // all work done, move to commit phase
}
}
(Modern React uses its own Scheduler package rather than requestIdleCallback directly, but the yielding principle is identical: work in small chunks, check the clock, hand control back to the browser if a higher-priority task, like a keystroke or a paint, needs to happen.)

For each fiber, performUnitOfWork does two things: it calls the component function (or diffs a host element) to figure out what the new children should be, and it reconciles those new children against the existing fiber tree.
This is where the actual "reconciliation" happens, the diffing you learned about early on. React compares the new element at a given position against the old fiber at that position using two heuristics, because a full tree diff is O(n³) and too slow to run on every update:
Two elements of different types produce entirely different trees. If a <div> becomes a <span>, React doesn't try to patch it. It tears down the old subtree and builds a new one from scratch, unmounting all state underneath.
Elements of the same type are compared by their key prop when they're part of a list. Without a stable key, React falls back to comparing by index, which is why reordering a keyed list without proper keys causes state to attach to the wrong item, inputs losing focus, checkboxes flipping to the wrong row, and so on. With a stable, unique key, React can tell that item 5 moved to position 2 rather than being destroyed and recreated.
Each fiber that needs a DOM mutation gets tagged with a flag, Placement for insert, Update for prop changes, Deletion for removal. Nothing touches the actual DOM yet. The render phase only builds up a list of side effects to apply later.
The Commit Phase: Where the DOM Actually Changes

Once the work-in-progress tree is fully built, React enters the commit phase, and this part is synchronous and cannot be interrupted. It has to be: you can't paint half-applied DOM mutations to the screen without visual tearing.
The commit phase runs in three sub-passes. First, a "before mutation" pass runs lifecycle methods like getSnapshotBeforeUpdate. Second, the mutation pass actually walks the effect list and applies DOM changes: inserting nodes, updating attributes, removing nodes, in the order needed to keep the DOM consistent. Third, a "layout" pass runs useLayoutEffect callbacks and lifecycle methods like componentDidMount and componentDidUpdate, synchronously, before the browser paints. This is why useLayoutEffect can read layout and mutate the DOM without visible flicker: it runs before paint. Regular useEffect callbacks are deferred and run after paint, asynchronously.
After commit, React flips the current tree pointer to the work-in-progress tree, and that tree becomes the current tree for the next render cycle.
Priority Lanes: Not All Updates Are Equal
Fiber made rendering interruptible, but interruptible alone doesn't tell React which work to prioritize when there's a choice. That's what lanes solve.
Every update in React is assigned to a lane, essentially a priority bucket represented as a bit in a 31-bit field, which allows React to combine and compare priorities using fast bitwise operations instead of arrays or numeric comparisons. A synchronous update, like text typed into a controlled input, goes into a high-priority lane, SyncLane. A startTransition update, like filtering a large list based on that same input, goes into a lower-priority TransitionLane.
When both are pending, React renders the high-priority lane first, and can pause a TransitionLane render entirely if a SyncLane update comes in, throwing away the incomplete work-in-progress tree for the transition and restarting once the urgent update is committed. This is the actual mechanism behind why typing in a filtered search box stays responsive even while the filtered list itself is expensive to compute: the keystroke and the list update are different priorities, not different threads.

React also prevents starvation. If a low-priority lane keeps getting preempted, React tracks how long it's been pending and eventually promotes it, folding it into the next synchronous pass so it's forced to complete rather than being deferred indefinitely.
function SearchableList({ items }) {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const [filtered, setFiltered] = useState(items);
function handleChange(e) {
setQuery(e.target.value); // SyncLane: urgent, stays responsive
startTransition(() => {
setFiltered(items.filter((i) => i.includes(e.target.value))); // TransitionLane
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending ? <Spinner /> : <List items={filtered} />}
</>
);
}
Practical Takeaway
When you're diagnosing a janky UI, the mental model to reach for isn't "React is slow," it's "which lane is this update in, and is the render phase getting interrupted enough to stay responsive." Stable keys aren't a lint rule to silence, they directly determine whether reconciliation reuses a subtree or tears it down. useLayoutEffect versus useEffect isn't a style choice, it's the difference between running before or after the commit phase's paint boundary. And startTransition doesn't make code faster, it changes which lane the resulting update lives in, letting more urgent work preempt it.
In interviews, this is also the difference between an answer that sounds memorized and one that shows you've actually reasoned about it. "React uses a virtual DOM to diff efficiently" is the memorized version. Explaining that Fiber turned a recursive, blocking tree walk into an interruptible linked-list traversal, and that lanes decide what work gets to interrupt what, is the version that holds up under a follow-up question.
Conclusion
Fiber didn't change what reconciliation computes, it changed how that computation is scheduled: as interruptible units of work instead of one blocking pass, prioritized by lane instead of processed in strict arrival order. Understanding that split, render phase builds a plan, commit phase executes it atomically, is what makes the rest of React's concurrent behavior predictable instead of magical.
Advertisement
Ready to practice?
Test your skills with our interactive UI challenges and build your portfolio.
Start Coding Challenge