Skip to content

Fiber Architecture Overview

advanced11 min read

Why Your Animation Stutters When You Filter a List

You've been there. You have a searchable product list with 5,000 items. The user types into the search input. On every keystroke, React re-renders the filtered list. During this render, the main thread is blocked for 200ms. The cursor freezes. The sidebar animation drops frames. Everything feels broken.

This is the exact problem Fiber was designed to solve. Before Fiber, React's reconciler was synchronous and recursive -- it started at the root, walked the entire tree, and couldn't stop until it was done. Fiber turned that into a loop that can pause after processing any single node.

function ProductSearch() {
  const [query, setQuery] = useState('');
  const filtered = products.filter(p =>
    p.name.toLowerCase().includes(query.toLowerCase())
  );

  return (
    <>
      <input onChange={e => setQuery(e.target.value)} />
      {/* Before Fiber: this blocks the main thread until all 5000 items render */}
      {/* With Fiber: React can pause here and handle your next keystroke */}
      {filtered.map(p => <ProductCard key={p.id} product={p} />)}
    </>
  );
}

The Mental Model

Mental Model

Think of React's old reconciler as a phone call you can't put on hold. Once it starts, you're committed until it's finished — even if something urgent comes in. Fiber turns reconciliation into a to-do list where each item is a small task. React works through the list one item at a time, and between items, it can check if something more important needs attention (like a user clicking a button). If so, it pauses the current list and handles the urgent task first.

Each item on the to-do list is a fiber node — a plain JavaScript object that represents one unit of work (one component, one DOM element, one fragment). The nodes are connected in a linked list, not a recursive call stack, so React can "bookmark" where it stopped and resume later.

From Stack to Fiber: Why React Had to Change

The Stack Reconciler Problem

React's original reconciler (pre-v16) used JavaScript's native call stack. When setState fired, React called render() on the root component, which called render() on children, which called render() on their children — recursively, all the way down.

// Stack reconciler call stack (simplified)
reconcile(App)
  → reconcile(Header)
    → reconcile(Logo)
    → reconcile(Nav)
  → reconcile(ProductSearch)
    → reconcile(Input)
    → reconcile(ProductList)
      → reconcile(ProductCard) × 5000  // Can't stop here

The call stack is all-or-nothing. There's no way to pause JavaScript execution mid-stack and resume later. The browser can't paint, handle events, or run animations until the entire recursive tree walk completes.

For small trees, this is invisible. For large trees, it's catastrophic: any render that takes more than 16ms (one frame at 60fps) causes dropped frames.

What Fiber Changes

Here's the key insight that makes Fiber possible. It replaces the recursive call stack with an iterative loop over a linked list. Each "frame" of work processes one fiber node, then returns control to the scheduler, which decides whether to continue or yield to the browser.

// Fiber work loop (conceptual)
while (nextUnitOfWork && !shouldYield()) {
  nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}

This single change makes rendering interruptible. React can process 50 fiber nodes, yield to the browser for animation frames, process 50 more, handle a click event, then finish the remaining nodes.

Fiber Node Anatomy

A fiber is just a plain JavaScript object. Nothing magical about it. Here's what it contains:

// Simplified fiber node structure
const fiber = {
  // Identity
  tag: FunctionComponent,    // What type of fiber (function, class, host, etc.)
  type: ProductCard,         // The component function/class, or 'div', 'span' for host
  key: 'product-42',         // Reconciliation key

  // Tree structure (linked list, NOT array children)
  child: null,               // First child fiber
  sibling: null,             // Next sibling fiber
  return: parentFiber,       // Parent fiber (called "return" because it's where
                             // processing returns to after this fiber completes)

  // State
  pendingProps: { product },  // Props for this render
  memoizedProps: { product }, // Props from last render
  memoizedState: null,        // Hook linked list (for function components)
  updateQueue: null,          // Queued state updates

  // Effects
  flags: Update | Placement,  // What side effects to commit
  subtreeFlags: 0,            // Aggregated child flags (optimization)

  // Double buffering
  alternate: currentFiber,    // The other version of this fiber (current ↔ workInProgress)
};
Why a linked list instead of arrays?

A tree with arrays of children looks like this:

{ type: 'div', children: [childA, childB, childC] }

Processing this recursively means: for each child, process it and all its descendants before moving to the next sibling. You can't stop mid-array.

A linked list looks like this:

childA.sibling = childB;
childB.sibling = childC;
childA.return = parent;
childB.return = parent;
childC.return = parent;
parent.child = childA;

Processing this iteratively means: process childA, then follow .sibling to childB, then to childC. At each step, you can stop and resume. The .return pointer lets you walk back up the tree without a call stack.

This is the key insight: linked lists turn tree traversal into a loop, and loops can be paused.

The Linked List Tree

The fiber tree uses three pointers per node: child, sibling, and return. This creates a structure that can be traversed without recursion:

        App
       /
    Header ——→ ProductSearch ——→ Footer
    /                /
  Logo ——→ Nav    Input ——→ ProductList
                               /
                        ProductCard ——→ ProductCard ——→ ...

Traversal order follows a depth-first pattern:

  1. Go to child (go deeper)
  2. If no child, go to sibling (go sideways)
  3. If no sibling, go to return (go back up) and try sibling from there
  4. Repeat until back at root
Execution Trace
Step 1:
App
Start at root. Has child → go to Header
Step 2:
Header
Has child → go to Logo
Step 3:
Logo
No child. Has sibling → go to Nav
Step 4:
Nav
No child, no sibling. Return to Header. Header has sibling → ProductSearch
Step 5:
ProductSearch
Has child → go to Input
Step 6:
Input
No child. Has sibling → go to ProductList
Step 7:
ProductList
Has child → go to first ProductCard
...:
ProductCards
Process each card via sibling links. Can YIELD between any two cards
Step N:
Footer
Last sibling of App's children. Process and complete

The traversal processes every node exactly once. And because it's a while loop instead of recursive calls, React can stop after any step and resume from the same position later.

Double Buffering: Current and WorkInProgress

This is one of the cleverest parts of the architecture. React maintains two fiber trees at all times:

  • Current tree: Represents what's currently on screen. This is the "committed" state.
  • WorkInProgress tree: The tree React is building during a render. This is the "draft."
Current tree           WorkInProgress tree
(on screen)            (being built)
     App ←——alternate——→ App
    /                     /
  Header ←—alternate—→ Header (reused — nothing changed)
    |                     |
  Nav ←————alternate——→ Nav (updated — new props)

When a render starts, React clones fiber nodes from the current tree into the workInProgress tree. Nodes that haven't changed are reused directly (the alternate pointer links them). Nodes that need updates get new fibers with updated props and state.

Once the entire workInProgress tree is built and all effects are computed, React "commits" it: the workInProgress tree becomes the new current tree. This swap is a single pointer reassignment — it's instantaneous.

// Conceptual commit
root.current = workInProgressTree;
// The old current tree becomes available for reuse in the next render

This double-buffering means:

  • The screen never shows a half-updated tree
  • React can discard a workInProgress tree if a higher-priority update interrupts it
  • Fiber nodes are recycled between renders (reduces GC pressure)

Production Scenario: The Dashboard That Dropped to 15fps

A monitoring dashboard renders 200 charts, each with a sparkline. Every 5 seconds, new data arrives and triggers a full re-render. During the render, the page freezes for 300ms — the charts don't animate, hover tooltips don't appear, and the navigation is unresponsive.

The team's first instinct was React.memo on every chart. That helped, but 80 charts still needed updates each cycle, and 80 chart renders still took 150ms synchronously.

The real fix was understanding Fiber's scheduling:

function Dashboard({ charts }) {
  const [urgentData, setUrgentData] = useState(null);
  const [chartData, setChartData] = useState(charts);

  useEffect(() => {
    const ws = new WebSocket('/api/stream');
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      // Urgent: update the alert banner immediately
      setUrgentData(data.alerts);
      // Deferred: update charts as a transition (interruptible)
      startTransition(() => {
        setChartData(data.charts);
      });
    };
    return () => ws.close();
  }, []);

  return (
    <>
      <AlertBanner data={urgentData} />  {/* Renders immediately */}
      <ChartGrid data={chartData} />     {/* Renders interruptibly */}
    </>
  );
}

With startTransition, the chart update is marked as a transition. Fiber processes the chart renders in chunks, yielding to the browser between chunks. The alert banner updates immediately (synchronous lane), hover tooltips still work, and animations stay at 60fps — even though the chart render takes the same total time.

Common Trap

Fiber doesn't make rendering faster. It makes rendering interruptible. The total work is the same — sometimes more, because of the overhead of yielding and resuming. The improvement is in perceived performance: the UI stays responsive during expensive renders because React shares the main thread instead of monopolizing it.

Common Mistakes

Common Mistakes
  • Wrong: Thinking Fiber makes renders faster by doing less work Right: Fiber makes renders interruptible — total work may increase slightly, but the UI stays responsive

  • Wrong: Assuming every render is interruptible by default Right: Only transitions (startTransition, useTransition) use interruptible rendering. Normal setState is synchronous

  • Wrong: Relying on render phase side effects (API calls, subscriptions in render) Right: Render must be pure. Side effects go in useEffect or event handlers

  • Wrong: Thinking the fiber tree mirrors the DOM tree Right: The fiber tree includes fragments, context providers, Suspense boundaries, and other non-DOM nodes

Challenge

Trace the fiber traversal

Show Answer

Traversal order:

  1. App — start at root, has child → go to Sidebar
  2. Sidebar — has child → go to Logo
  3. Logo — no child, has sibling → go to Menu
4. **Menu** — no child, no sibling → return to Sidebar. Sidebar has sibling → go to Main
  1. Main — has child → go to Article
  2. Article — has child → go to Title
  3. Title — no child, has sibling → go to Body
8. **Body** — no child, no sibling → return to Article → return to Main → return to App → done

Total: 8 units of work. React can yield between any two steps. If a user clicks during step 4, React can pause, handle the click, then resume at step 5.

Quiz

Quiz
Why does React use a linked list (child/sibling/return pointers) instead of arrays of children?
Quiz
What happens to the workInProgress tree if a higher-priority update arrives mid-render?

Key Rules

Key Rules
  1. 1Fiber replaced React's recursive stack reconciler with an iterative linked-list traversal that can pause and resume.
  2. 2Each fiber node is a plain object with child, sibling, and return pointers. The tree is traversed depth-first via a while loop.
  3. 3React maintains two trees: current (on screen) and workInProgress (being built). Commit swaps them atomically.
  4. 4Fiber enables time-slicing: React processes work in chunks, yielding to the browser between chunks to maintain 60fps.
  5. 5Fiber doesn't make rendering faster — it makes it interruptible. Total work stays the same or slightly increases.
  6. 6Render functions must be pure because Fiber can abort, restart, or duplicate render calls.
1/11