React's Fiber Architecture: How React Became Interruptible
Before React 16, the reconciler was a simple recursive function. It walked the component tree, diffed the output, and applied changes to the DOM synchronously. Once it started, it couldn't stop. On a large tree with a slow component, nothing else — no user input, no animations, no UI updates — could happen until reconciliation finished.
Fiber was the complete rewrite of that reconciler, released in React 16. It replaced the recursive stack with a linked list of units of work, making rendering interruptible, resumable, and prioritised. Everything React shipped since — Concurrent Mode, useTransition, Suspense, Server Components — is built on Fiber.
The Problem: Synchronous Rendering Blocked the Main Thread
The old reconciler was a depth-first recursive tree walk. Once started, it held the call stack until it finished:
renderComponent(App)
→ renderComponent(Layout)
→ renderComponent(Header)
→ renderComponent(Nav)
→ ...
On a fast machine with a small tree, this was fine. On a slow device with a thousand-node tree and some expensive computations, the main thread could be blocked for hundreds of milliseconds — dropping frames, making inputs feel sluggish, and ruining Core Web Vitals.
The fundamental issue: a recursive call stack can't be paused mid-execution.
Fiber Nodes: Units of Work
Fiber replaces the call stack with a linked list of fiber nodes. Each component in the tree corresponds to a fiber — a plain JavaScript object that represents a unit of work:
// Simplified fiber node
{
type: MyComponent,
key: null,
stateNode: <actual DOM node or component instance>,
return: <parent fiber>,
child: <first child fiber>,
sibling: <next sibling fiber>,
pendingProps: { ... },
memoizedProps: { ... },
memoizedState: { ... },
effectTag: 'UPDATE' | 'PLACEMENT' | 'DELETION',
nextEffect: <next fiber with side effects>,
lanes: <bitmask of pending work priority>,
}
Instead of a recursive function, React's work loop processes one fiber at a time in a loop:
function workLoop() {
while (currentFiber !== null) {
currentFiber = performUnitOfWork(currentFiber)
// After each unit, the scheduler can interrupt here
if (shouldYield()) break
}
}
After processing each fiber, the scheduler checks whether higher-priority work has come in (e.g. a user typing). If so, it yields. When the main thread is free, it resumes from where it left off.
Two Phases: Render and Commit
Fiber splits reconciliation into two distinct phases:
Render phase (interruptible): React walks the fiber tree, calling render functions, computing the diff, and collecting effects (DOM mutations needed). This phase can be paused and resumed — React may throw away partial work and restart if higher-priority updates arrive.
Commit phase (synchronous, non-interruptible): React takes the complete list of mutations and applies them to the real DOM in a single synchronous pass. This phase cannot be interrupted because a half-applied DOM update would leave the UI in an inconsistent state.
This is why React strict mode runs the render phase twice in development: to surface side effects that rely on render happening exactly once, since the real-world render phase may be thrown away and restarted.
Work Lanes: Priority Queues
Fiber assigns a lane to every piece of work — a bitmask representing priority. React has approximately 30 lanes, grouped into:
- SyncLane — synchronous, must run before paint (e.g. user input like typing)
- InputContinuousLane — continuous events (e.g. scroll, pointermove)
- DefaultLane — normal state updates
- TransitionLane — low-priority updates marked with
startTransition - OffscreenLane — pre-rendering work (e.g.
<Offscreen>)
When you call startTransition, React marks the update as a transition lane and will interrupt it if a higher-priority update (like typing in an input) arrives. This is what makes the useTransition hook possible:
const [isPending, startTransition] = useTransition()
function handleInput(e) {
// High priority — updates immediately
setInputValue(e.target.value)
// Low priority — can be interrupted
startTransition(() => {
setSearchResults(search(e.target.value))
})
}
Without Fiber's lane system, there's no way to express "update the input immediately but the list can wait."
The Alternate Tree
Fiber maintains two trees at all times:
- Current tree — represents what's currently rendered on screen
- Work-in-progress tree — the tree React is building in the render phase
When the commit phase completes, the work-in-progress tree becomes the current tree (double buffering). If the render phase is interrupted, the partial work-in-progress tree is discarded — the current tree is still valid.
What Fiber Unlocks
Fiber is the prerequisite for every advanced React feature:
- Concurrent rendering — multiple renders in flight at different priorities
Suspense— React can hold a tree in a "suspended" state while waiting for datauseTransition/useDeferredValue— explicit priority control for developers- Server Components — rendering on the server with a streaming protocol that Fiber can consume
- Offscreen rendering — pre-rendering hidden UI without blocking visible work
Conclusion
Fiber changed React's reconciler from a synchronous, uninterruptible recursive call to a loop over a linked list of prioritised work units. This single architectural change — separating the render phase (interruptible) from the commit phase (synchronous) — is what makes every modern React performance feature possible. Understanding Fiber explains why startTransition works, why strict mode double-invokes render, and why React's Concurrent Mode can handle large trees without dropping frames.
