React's Fiber Architecture: How React Became Interruptible

Rendering & Frameworks

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 data
  • useTransition / 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.