React’s Fiber Architecture – The Secret Behind Interruptible Rendering (And Why It’s Brilliant)



This content originally appeared on DEV Community and was authored by Mohamad Msalme

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>
  );
}

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
}

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

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!

💡 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
    }
  }
};

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

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>
  );
}

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
}

🎯 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

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)
}

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;
  }
}

🔄 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
}

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!

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
}

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! 😱

🎯 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>
  );
}

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!

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


This content originally appeared on DEV Community and was authored by Mohamad Msalme