Deep Dive: React Reconciliation and Fiber Architecture

Today

Understanding React Reconciliation and Fiber Architecture

React's rendering engine is one of the most sophisticated pieces of the modern web stack. At its core lies reconciliation and the Fiber architecture — two concepts that fundamentally changed how React performs and handles complex UIs. This guide provides a deep technical understanding of how these systems work together.

The Problem: Synchronous Rendering

Before diving into Fiber, it's crucial to understand why it was needed. In React's early days (pre-16.0), the reconciliation process was entirely synchronous. Here's what that meant:

// Old Stack Reconciliation (Pre-Fiber)
function render(element) {
  // If you call render, nothing else runs until it's done
  // The main thread is blocked
  // Browser can't handle user input, animations, or other tasks
}

When React started rendering a tree, it couldn't stop mid-way. This caused jank — the UI would freeze when:

The Synchronous Stack Problem

Main Thread Timeline (Synchronous)
[================== React Render ====================] [User Input] [Paint]
                    ^                                  ^
                    Blocked for entire render        Now you get input
                    
Result: Janky UI, dropped frames, poor animation quality

Introducing React Fiber (v16.0+)

React Fiber fundamentally changed this by introducing incremental rendering — the ability to:

  1. Pause work and come back to it later
  2. Abort work if it's no longer needed
  3. Prioritize different types of work
  4. Reuse previously completed work

What is a Fiber?

A Fiber is a JavaScript object representing a component instance, DOM node, or hook state. It's the fundamental unit of work in React's reconciliation engine.

// Simplified Fiber structure
interface Fiber {
  // Identifiers
  type: string | Function;        // Component type or tag name
  key: string | null;             // React key for list items
  ref: any;                        // User-provided ref
  
  // Component data
  props: any;                      // Props passed to component
  state: any;                      // Component state
  memoizedState: any;              // Cached state for hooks
  
  // Tree structure
  parent: Fiber | null;            // Parent fiber
  child: Fiber | null;             // First child
  sibling: Fiber | null;           // Next sibling
  
  // Effect tracking
  effectTag: string;               // 'Placement' | 'Update' | 'Deletion'
  effects: Fiber[];                // Effects to run
  
  // Work scheduling
  expirationTime: number;          // When this work expires
  childExpirationTime: number;     // When children work expires
  
  // Reconciliation data
  alternate: Fiber | null;         // Previous version of this fiber
}

The Fiber Tree Structure

React maintains two trees during reconciliation:

// Current Fiber Tree (what's rendered)
const currentRoot = {
  type: 'div',
  props: { className: 'app' },
  child: {
    type: Counter,
    state: { count: 0 },
    child: {
      type: 'button',
      props: { children: 'Count: 0' }
    }
  }
};
 
// Work-in-Progress Tree (being reconciled)
const wipRoot = {
  type: 'div',
  props: { className: 'app' },
  child: {
    type: Counter,
    state: { count: 1 },  // Updated!
    child: {
      type: 'button',
      props: { children: 'Count: 1' }  // Updated!
    }
  },
  alternate: currentRoot  // Points back to current tree
};

The Reconciliation Algorithm

React's reconciliation process uses a two-phase approach:

Phase 1: Render Phase (Can be Paused)

During this phase, React:

  1. Compares old and new fiber trees (diffing)
  2. Marks what needs to change
  3. Creates the work-in-progress tree

This is the interruptible phase where React can pause to let the browser handle user input or animations.

// Simplified reconciliation logic
function reconcile(fiber: Fiber, newProps: any): Fiber {
  // Step 1: Compare props
  if (!propsChanged(fiber.props, newProps)) {
    return fiber;  // No changes needed
  }
  
  // Step 2: Mark as needing update
  const newFiber = {
    ...fiber,
    props: newProps,
    effectTag: 'Update',
    alternate: fiber  // Reference old fiber for comparison
  };
  
  // Step 3: Recursively reconcile children
  reconcileChildren(newFiber, newProps.children);
  
  return newFiber;
}
 
// Simplified reconciliation loop
let nextFiber = wipRoot;
let deadline = getTimeRemaining();  // Browser gives us time
 
while (nextFiber && deadline > 0) {
  nextFiber = performUnitOfWork(nextFiber);
  deadline = getTimeRemaining();
  
  // If time's up, pause and let browser work
  if (deadline <= 0) {
    scheduleCallback(performWork);  // Schedule continuation
    break;
  }
}

Phase 2: Commit Phase (Not Interruptible)

Once all render phase work is done, React commits the changes to the DOM in one go:

// Simplified commit phase
function commitAllWork(fiber: Fiber) {
  // Phase 2 is synchronous - once started, completes fully
  
  // Step 1: Run all effects (setState, etc.)
  commitEffects(fiber);
  
  // Step 2: Attach to DOM
  fiber.stateNode = createInstance(fiber);
  
  // Step 3: Update DOM nodes
  if (fiber.effectTag === 'Placement') {
    commitPlacement(fiber);
  } else if (fiber.effectTag === 'Update') {
    commitUpdate(fiber);
  } else if (fiber.effectTag === 'Deletion') {
    commitDeletion(fiber);
  }
  
  // Step 4: Recursively commit children
  commitAllWork(fiber.child);
  commitAllWork(fiber.sibling);
}

Work Prioritization and Scheduling

One of Fiber's superpowers is priority scheduling. Not all updates are equal:

// Priority levels in React
const ImmediatePriority = 99;      // User interactions (clicks)
const UserBlockingPriority = 98;   // Animations, form inputs
const NormalPriority = 97;         // Data fetching, timers
const LowPriority = 96;            // Non-essential updates
const IdlePriority = 95;           // Lowest priority work
 
// React scheduler assigns priorities
function scheduleWork(fiber: Fiber, expirationTime: number) {
  if (expirationTime === ImmediatePriority) {
    // Do it NOW - user clicked
    performWork();
  } else if (expirationTime === UserBlockingPriority) {
    // Schedule ASAP - animation frame coming
    scheduleCallback(performWork);
  } else {
    // Can wait - low priority update
    scheduleCallback(performWork, { delay: 5000 });
  }
}
 
// Example: User interaction has higher priority than data fetch
function App() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState(null);
  
  const handleClick = () => {
    // This update gets ImmediatePriority
    setCount(count + 1);
    
    // This update gets NormalPriority
    fetchData().then(setData);
  };
  
  return <button onClick={handleClick}>Count: {count}</button>;
}

The Diffing Algorithm (Reconciliation Rules)

React uses smart heuristics to compare trees efficiently:

Rule 1: Same Element Type = Update

// Old tree
<div className="a" />
 
// New tree
<div className="b" />
 
// Result: Update the existing div
// Reuse DOM node, update className

Rule 2: Different Element Type = Replace

// Old tree
<div />
 
// New tree
<span />
 
// Result: Destroy div fiber, create new span fiber
// Cannot reuse DOM node

Rule 3: Keys Help with Lists

// Without keys (inefficient)
function List({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li>{item}</li>  // Position-based matching
      ))}
    </ul>
  );
}
 
// Old: ['a', 'b', 'c']
// New: ['b', 'c', 'd']
// React thinks: 'a' → 'b', 'b' → 'c', 'c' → 'd'
// All 3 items need re-rendering!
 
// With keys (efficient)
function List({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>  // Identity-based
      ))}
    </ul>
  );
}
 
// React knows: 'a' stays, 'b' moved, 'c' moved, 'd' added
// Only new item needs rendering

Performance: Before and After Fiber

Before Fiber (Synchronous)

Frame Time: 16ms (60fps target)

Scenario: Complex form with 50 input fields
[====== React Render (22ms) ======]  <- Exceeds frame budget
[USER CAN'T INTERACT]
[Frame drops, animations stutter]

After Fiber (Incremental)

Frame Time: 16ms (60fps target)

Scenario: Same complex form
[== Render Work ==] [Browser Paint] [Animation] [== Continue ==]
5ms                 5ms             2ms         4ms total
[USER CAN INTERACT]
[Smooth 60fps maintained]

Practical Code Example: Custom Hook with Fiber Concepts

Understanding Fiber helps you write better React code:

// useCallback memoization relies on Fiber's effect tracking
function memoizeCallback<T extends (...args: any[]) => any>(
  callback: T,
  deps: any[]
): T {
  // React stores this in fiber's memoizedState
  // Fiber knows when to invalidate the memoization
  return useCallback(callback, deps);
}
 
// useMemo works similarly
function useExpensiveComputation(deps: any[]) {
  return useMemo(() => {
    // Fiber tracks if deps changed
    // Skips recomputation if unchanged
    return expensiveOperation();
  }, deps);
}
 
// Understanding Fiber helps you batch updates
function useOptimizedState<T>(initial: T) {
  const [state, setState] = useState(initial);
  
  // Fiber scheduler will batch these automatically
  const handleMultipleUpdates = () => {
    setState(s => s + 1);      // Batched
    setState(s => s + 1);      // Batched
    setState(s => s + 1);      // Batched
    // All 3 updates execute in single render phase
  };
  
  return [state, handleMultipleUpdates];
}

Key Takeaways

  1. Fiber is React's runtime - the JavaScript object representation of your component tree
  2. Reconciliation is incremental - React can pause and resume work to keep the UI responsive
  3. Two phases - Render phase (pausable) and Commit phase (atomic)
  4. Priority scheduling - User interactions get prioritized over background work
  5. Efficient diffing - Keys help React understand which items moved in lists
  6. Practical impact - Better performance with automatic batching and interruptible rendering

This understanding helps you write more performant React applications and debug rendering issues more effectively!

Further Reading