DEV Community

Cover image for React's Fiber Architecture - The Secret Behind Interruptible Rendering (And Why It's Brilliant)
Mohamad Msalme
Mohamad Msalme

Posted on

React's Fiber Architecture - The Secret Behind Interruptible Rendering (And Why It's Brilliant)

Know WHY β€” Let AI Handle the HOW πŸ€–

In Part 1, we learned how React prioritizes updates using useDeferredValue. But here's the question that should be bugging you: How does React actually pause rendering mid-way and resume later?

What if I told you it's not magic - it's actually a clever data structure called Fiber that transforms your component tree into something React can pause, resume, and even throw away? Understanding this changes how you think about React performance forever.

πŸ€” The Problem React Had to Solve

Before we get to Fibers, let's understand the problem:

function App() {
  return (
    <div>
      <Header />
      <MainContent>
        <Article />
        <Sidebar>
          <Widget1 />
          <Widget2 />
          <Widget3 />
        </Sidebar>
      </MainContent>
      <Footer />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Old React (before Fiber):

  • Renders from top to bottom recursively
  • Once started, MUST finish the entire tree
  • No way to pause halfway through
  • If user clicks during render, too bad - they wait

The Challenge: How do you make rendering interruptible without breaking everything?

🧠 Think Like a Book Reader for a Moment

Imagine you're reading a book:

Without Bookmarks (Old React):

  • Start chapter 1
  • Must read straight through to the end
  • Can't stop mid-sentence
  • Can't remember where you were if interrupted

With Bookmarks (Fiber Architecture):

  • Start chapter 1
  • Can place a bookmark on any page
  • Stop reading when something urgent comes up
  • Resume exactly where you left off
  • Can even decide "this chapter isn't relevant anymore" and skip it

That's exactly what Fiber does for React rendering.

πŸ”‘ What Is a Fiber? The Unit of Work

A Fiber is a JavaScript object that represents one unit of work in React. Each React element becomes a Fiber node.

// Your JSX
<DisplayCounter count={5} />

// Becomes a Fiber object (simplified):
{
  // Identity
  type: DisplayCounter,        // Component function/class
  key: null,                   // React key

  // Tree structure (linked list!)
  return: parentFiber,         // Parent (going up)
  child: firstChildFiber,      // First child (going down)
  sibling: nextSiblingFiber,   // Next sibling (going across)

  // Props and State
  pendingProps: { count: 6 },  // New props to apply
  memoizedProps: { count: 5 }, // Current props
  memoizedState: null,         // Current state

  // THE KEY TO CONCURRENT MODE!
  lanes: 0b0001,              // Priority (binary flag)
  childLanes: 0b0011,         // Children's priorities

  // Effects (what needs to happen)
  flags: Update,              // Bitwise flags

  // Double buffering
  alternate: otherVersionOfThisFiber
}
Enter fullscreen mode Exit fullscreen mode

Why Linked List Instead of Array?

Array (Old React):

const tree = [
  { type: 'div', children: [
    { type: Header },
    { type: MainContent, children: [...] }
  ]}
];

// Problem: Can't easily pause mid-traversal
// Would need complex index tracking
Enter fullscreen mode Exit fullscreen mode

Linked List (Fiber):

const fiber = {
  type: 'div',
  child: headerFiber,  // ← Can pause here
};

const headerFiber = {
  type: Header,
  return: fiber,       // ← Know where we came from
  sibling: mainFiber,  // ← Know where to go next
};

// Can pause at any fiber and resume later!
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ The Brilliant Part: Double Buffering

React maintains TWO complete fiber trees at all times:

  1. Current Tree - What's displayed on screen right now
  2. Work-in-Progress Tree - What React is building
// Current tree (what user sees)
const currentTree = {
  type: App,
  child: {
    type: 'div',
    child: {
      type: DisplayCounter,
      props: { count: 5 },
      alternate: wipFiber  // ← Points to work-in-progress version
    }
  }
};

// Work-in-progress tree (what React is building)
const workInProgressTree = {
  type: App,
  child: {
    type: 'div',
    child: {
      type: DisplayCounter,
      props: { count: 6 },  // ← New count!
      alternate: currentFiber  // ← Points back to current
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Why This Is Genius:

// React can work on work-in-progress tree
// User still sees current tree (stable UI)

// If interrupted by urgent update:
// β†’ Just throw away work-in-progress tree
// β†’ Start fresh with new priorities
// β†’ No harm done!

// When rendering completes:
// β†’ Swap pointers in ONE atomic operation
// β†’ current = workInProgress
// β†’ workInProgress = current
Enter fullscreen mode Exit fullscreen mode

Real Example: Updating a Counter

function App() {
  const [count, setCount] = useState(5);
  const deferredCount = useDeferredValue(count);

  return (
    <div>
      <DisplayCounter count={count} />         {/* Fiber A */}
      <ExpensiveList count={deferredCount} />  {/* Fiber B */}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

After user clicks (count becomes 6):

// CURRENT TREE (still visible on screen)
Fiber A: {
  type: DisplayCounter,
  props: { count: 5 },  // Old value
  lanes: 0b0000,
}

Fiber B: {
  type: ExpensiveList,
  props: { count: 5 },  // Old value
  lanes: 0b0000,
}

// WORK-IN-PROGRESS TREE (being built)
Fiber A (WIP): {
  type: DisplayCounter,
  props: { count: 6 },     // New value!
  lanes: 0b0001,           // SyncLane - HIGH PRIORITY
  alternate: currentFiberA // Points to current tree
}

Fiber B (WIP): {
  type: ExpensiveList,
  props: { count: 5 },     // Still old (deferred!)
  lanes: 0b1000,           // TransitionLane - LOW PRIORITY
  alternate: currentFiberB
}
Enter fullscreen mode Exit fullscreen mode

🎯 Priority Lanes: How React Tracks Urgency

React uses binary flags for lightning-fast priority checks.

// Priority lanes (actual React source - simplified)
const SyncLane            = 0b0000000000000000000000000000001; // Highest
const InputContinuousLane = 0b0000000000000000000000000000100;
const DefaultLane         = 0b0000000000000000000000000010000;
const TransitionLane      = 0b0000000000000000000001000000000; // Low priority
const IdleLane            = 0b0100000000000000000000000000000; // Lowest

// Why binary? Super fast operations:
const hasUrgentWork = (lanes & SyncLane) !== 0;  // Single CPU instruction!
const combinedWork = lanes1 | lanes2;            // Merge priorities instantly
Enter fullscreen mode Exit fullscreen mode

How useDeferredValue Sets Lanes

function App() {
  const [count, setCount] = useState(0);
  const deferredCount = useDeferredValue(count);

  // When count updates:
  // 1. Components using `count` get SyncLane (0b0001)
  // 2. Components using `deferredCount` get TransitionLane (0b1000)
}
Enter fullscreen mode Exit fullscreen mode

Internal priority assignment:

// Simplified React internals
function scheduleUpdate(fiber, newValue, isDeferred) {
  if (isDeferred) {
    // LOW PRIORITY - can be interrupted
    fiber.lanes = TransitionLane;
    fiber.pendingProps = newValue;
  } else {
    // HIGH PRIORITY - process immediately
    fiber.lanes = SyncLane;
    fiber.pendingProps = newValue;
  }

  // Bubble priority up to root
  let parent = fiber.return;
  while (parent) {
    parent.childLanes |= fiber.lanes;  // Bitwise OR combines lanes
    parent = parent.return;
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”„ The Two Phases of Rendering

React's work is split into two distinct phases:

Phase 1: Render Phase (Interruptible βœ…)

This is where React can pause.

function workLoopConcurrent() {
  // THE KEY: This loop can be interrupted!
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }

  // If shouldYield() returns true, we pause here
  // workInProgress remembers where we stopped
  // We can resume later from this exact fiber!
}

function performUnitOfWork(fiber) {
  // Step 1: Call the component
  const children = fiber.type(fiber.props);

  // Step 2: Reconcile (diff) children
  reconcileChildren(fiber, children);

  // Step 3: Return next unit of work
  if (fiber.child) return fiber.child;      // Go down
  if (fiber.sibling) return fiber.sibling;  // Go across
  return fiber.return;                      // Go up
}
Enter fullscreen mode Exit fullscreen mode

Example: Rendering Pauses Mid-Tree

<App>
  <Header />              {/* βœ… Rendered */}
  <MainContent>
    <Article />           {/* βœ… Rendered */}
    <Sidebar>
      <Widget1 />         {/* βœ… Rendered */}
      <Widget2 />         {/* ⏸️ PAUSE HERE - shouldYield() = true */}
      <Widget3 />         {/* ⏳ Not rendered yet */}
    </Sidebar>
  </MainContent>
  <Footer />              {/* ⏳ Not rendered yet */}
</App>

// React: "Used my 5ms time slice, better let browser handle user input"
// workInProgress = Widget2Fiber
// Can resume from here later!
Enter fullscreen mode Exit fullscreen mode

Key Characteristics:

  • βœ… No DOM mutations yet
  • βœ… Can pause at any fiber
  • βœ… Can resume where left off
  • βœ… Can throw away all work if urgent update arrives
  • βœ… Pure computations only

Phase 2: Commit Phase (Uninterruptible ❌)

Once render phase completes, React commits changes atomically.

function commitRoot(root) {
  const finishedWork = root.finishedWork;

  // THIS PHASE CANNOT BE INTERRUPTED!
  // Must complete to avoid inconsistent UI

  // Sub-phase 1: Before mutation
  commitBeforeMutationEffects(finishedWork);
  // β†’ Calls getSnapshotBeforeUpdate
  // β†’ Schedules useEffect

  // Sub-phase 2: Mutation (THE CRITICAL MOMENT)
  commitMutationEffects(finishedWork);
  // β†’ Actual DOM updates happen here
  // β†’ All at once, atomically

  // Sub-phase 3: Switch trees (ATOMIC SWAP)
  root.current = finishedWork;
  // ↑ NOW the new tree is visible!

  // Sub-phase 4: Layout effects
  commitLayoutEffects(finishedWork);
  // β†’ Calls useLayoutEffect
  // β†’ Calls componentDidMount/Update
}
Enter fullscreen mode Exit fullscreen mode

Why Must Commit Be Uninterruptible?

// Imagine if commit could pause mid-way:
function UserProfile() {
  return (
    <div>
      <Avatar src={user.avatar} />
      <Name>{user.name}</Name>
      <Email>{user.email}</Email>
    </div>
  );
}

// If paused after Avatar but before Name/Email:
// DOM shows:
//   Avatar for User B
//   Name for User A  ← INCONSISTENT!
//   Email for User A ← INCONSISTENT!
// 
// Users would see mixed data! 😱
Enter fullscreen mode Exit fullscreen mode

🎯 Real-World Example: Search with Interruption

function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  const results = useMemo(() => {
    // Expensive filtering
    return bigDataset.filter(item => 
      item.name.includes(deferredQuery)
    );
  }, [deferredQuery]);

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <ResultsList items={results} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

What happens internally when you type "react":

User types "r":
0ms:  query = "r"
1ms:  RENDER PHASE (HIGH PRIORITY)
      β†’ Input fiber: lanes = SyncLane
      β†’ Render <input value="r" /> βœ…
2ms:  COMMIT PHASE
      β†’ Update DOM, input shows "r" βœ…
3ms:  RENDER PHASE (LOW PRIORITY)
      β†’ ResultsList fiber: lanes = TransitionLane
      β†’ Start filtering for "r"
      β†’ ResultsList fiber #1 rendering...
      β†’ ResultsList fiber #2 rendering...

10ms: User types "e" (NEW HIGH PRIORITY UPDATE!)
11ms: shouldYield() = true (time to check for urgent work)
12ms: React finds SyncLane work waiting
13ms: ABANDON work-in-progress tree (throw it away!)
14ms: RENDER PHASE (HIGH PRIORITY)
      β†’ Input fiber: lanes = SyncLane
      β†’ Render <input value="re" /> βœ…
15ms: COMMIT PHASE
      β†’ Update DOM, input shows "re" βœ…
16ms: RENDER PHASE (LOW PRIORITY)
      β†’ ResultsList: START NEW render for "re"
      β†’ Old "r" filter abandoned, never committed!
Enter fullscreen mode Exit fullscreen mode

The user never sees results for "r" - React intelligently skipped that intermediate state!

🏠 The Perfect Analogy: Construction Site with Blueprints

Think of React's Fiber system like a construction site:

Current Tree = The actual building people are using
Work-in-Progress Tree = Blueprint and temporary scaffolding

  • Workers can modify the blueprint/scaffolding all day
  • People in the building don't see any changes (stable!)
  • If plans change, just throw away the scaffolding
  • When blueprint is done, do the final construction (commit phase)
  • Switch happens all at once - no half-renovated rooms

🧠 The Mental Model Shift

Stop Thinking:

  • "React renders top to bottom"
  • "Once rendering starts, it must finish"
  • "State updates are immediate"

Start Thinking:

  • "React renders fiber by fiber (unit by unit)"
  • "React can pause between any two fibers"
  • "React builds a new tree in the background"
  • "Priorities determine which fibers render first"
  • "Only commit phase makes changes visible"

πŸ’­ The Takeaway

Many developers learn the HOW: "Use React hooks and it works."

When you understand the WHY: "React uses Fiber nodes in a linked list structure with priority lanes, enabling interruptible rendering through double buffering," you gain insights that help you:

  • Understand why some updates feel instant and others don't
  • Know when to use useDeferredValue vs useTransition
  • Debug performance issues by thinking about fiber priorities
  • Make better architectural decisions

Top comments (0)