Steve Kinney

Virtual DOM & Fiber Architecture

You’ve heard it a thousand times:

React uses a Virtual DOM for performance.

But when pressed for details, most developers hand-wave something about “diffing” and “batching updates.” The truth is far more fascinating. React’s Fiber architecture is a complete rewrite of React’s core algorithm, introducing concepts like time-slicing, priority scheduling, and interruptible rendering that fundamentally change how we should think about React performance.

Understanding Fiber isn’t just academic curiosity—it directly impacts how you write performant React code. When you know how React schedules work, prioritizes updates, and decides what to render when, you can structure your components and state updates to work with React’s algorithm instead of against it. This deep dive reveals the inner workings that power every React application.

The Virtual DOM: More Than a Feeling

The Virtual DOM isn’t just a performance optimization—it’s a programming model that enables declarative UI:

// What the Virtual DOM actually is
interface VirtualNode {
  type: string | ComponentType; // 'div' or MyComponent
  props: Record<string, any>; // Including children
  key: string | null; // For list reconciliation
  ref: any; // Ref attachments
}

// Simplified Virtual DOM creation
function createElement(
  type: string | ComponentType,
  props: Record<string, any> | null,
  ...children: any[]
): VirtualNode {
  return {
    type,
    props: {
      ...props,
      children: children.length === 1 ? children[0] : children,
    },
    key: props?.key || null,
    ref: props?.ref || null,
  };
}

// What JSX compiles to
const jsxElement = <div className="card">Hello</div>;

// Becomes:
const virtualElement = createElement('div', { className: 'card' }, 'Hello');

// Component Virtual DOM
const ComponentVDOM = createElement(
  MyComponent,
  { name: 'React' },
  createElement('span', null, 'Child'),
);

The Reconciliation Process

// Simplified reconciliation algorithm
class SimpleReconciler {
  reconcile(
    oldVNode: VirtualNode | null,
    newVNode: VirtualNode | null,
    container: HTMLElement,
  ): void {
    // Deletion
    if (oldVNode && !newVNode) {
      this.removeNode(oldVNode, container);
      return;
    }

    // Addition
    if (!oldVNode && newVNode) {
      this.createNode(newVNode, container);
      return;
    }

    // Update
    if (oldVNode && newVNode) {
      // Different types: replace entirely
      if (oldVNode.type !== newVNode.type) {
        this.removeNode(oldVNode, container);
        this.createNode(newVNode, container);
        return;
      }

      // Same type: update properties and recurse on children
      this.updateNode(oldVNode, newVNode, container);
      this.reconcileChildren(oldVNode, newVNode, container);
    }
  }

  private reconcileChildren(
    oldVNode: VirtualNode,
    newVNode: VirtualNode,
    container: HTMLElement,
  ): void {
    const oldChildren = Array.isArray(oldVNode.props.children)
      ? oldVNode.props.children
      : [oldVNode.props.children];

    const newChildren = Array.isArray(newVNode.props.children)
      ? newVNode.props.children
      : [newVNode.props.children];

    // Key-based reconciliation for lists
    if (this.hasKeys(newChildren)) {
      this.reconcileKeyedChildren(oldChildren, newChildren, container);
    } else {
      // Index-based reconciliation (less efficient)
      this.reconcileIndexedChildren(oldChildren, newChildren, container);
    }
  }

  private reconcileKeyedChildren(
    oldChildren: VirtualNode[],
    newChildren: VirtualNode[],
    container: HTMLElement,
  ): void {
    // Build key maps for efficient lookup
    const oldKeyMap = new Map<string, VirtualNode>();
    oldChildren.forEach((child) => {
      if (child.key) oldKeyMap.set(child.key, child);
    });

    newChildren.forEach((newChild, index) => {
      const oldChild = oldKeyMap.get(newChild.key!);

      if (oldChild) {
        // Reuse existing node
        this.updateNode(oldChild, newChild, container);
        oldKeyMap.delete(newChild.key!);
      } else {
        // Create new node
        this.createNode(newChild, container);
      }
    });

    // Remove unused old nodes
    oldKeyMap.forEach((oldChild) => {
      this.removeNode(oldChild, container);
    });
  }
}

Fiber Architecture: The Game Changer

Fiber reimagines React’s reconciliation algorithm as an incremental, interruptible process:

// Fiber node structure (simplified)
interface FiberNode {
  // Instance
  tag: WorkTag; // Component type (Function, Class, Host, etc.)
  type: any; // Component constructor or DOM tag
  stateNode: any; // DOM node or class instance

  // Props and state
  pendingProps: any; // New props
  memoizedProps: any; // Previous props
  memoizedState: any; // Current state
  updateQueue: UpdateQueue | null; // Pending state updates

  // Effects
  flags: Flags; // Side effects to perform
  subtreeFlags: Flags; // Aggregated flags from subtree
  deletions: FiberNode[] | null; // Children to delete

  // Tree structure
  return: FiberNode | null; // Parent fiber
  child: FiberNode | null; // First child
  sibling: FiberNode | null; // Next sibling
  index: number; // Position in parent

  // Work in progress
  alternate: FiberNode | null; // Double buffering

  // Time and priority
  lanes: Lanes; // Priority of this update
  childLanes: Lanes; // Priority of children
  expirationTime: number; // When this work expires
}

// Work tags identify fiber type
enum WorkTag {
  FunctionComponent = 0,
  ClassComponent = 1,
  HostRoot = 3, // Root of the tree
  HostComponent = 5, // DOM elements
  HostText = 6, // Text nodes
  Fragment = 7,
  Mode = 8,
  ContextConsumer = 9,
  ContextProvider = 10,
  ForwardRef = 11,
  Profiler = 12,
  SuspenseComponent = 13,
  MemoComponent = 14,
  SimpleMemoComponent = 15,
  LazyComponent = 16,
}

// Flags track side effects
enum Flags {
  NoFlags = 0b0000000000000000000,
  PerformedWork = 0b0000000000000000001,
  Placement = 0b0000000000000000010,
  Update = 0b0000000000000000100,
  Deletion = 0b0000000000000001000,
  ChildDeletion = 0b0000000000000010000,
  ContentReset = 0b0000000000000100000,
  Callback = 0b0000000000001000000,
  DidCapture = 0b0000000000010000000,
  Ref = 0b0000000000100000000,
  Snapshot = 0b0000000001000000000,
  Passive = 0b0000000010000000000,
  LayoutMask = Update | Callback | Ref | Snapshot,
  PassiveMask = Passive | ChildDeletion,
}

The Fiber Work Loop

// Simplified Fiber work loop
class FiberScheduler {
  private workInProgressRoot: FiberNode | null = null;
  private workInProgress: FiberNode | null = null;
  private remainingExpirationTime: number = NoWork;
  private nextUnitOfWork: FiberNode | null = null;

  // Main work loop - can be interrupted
  workLoop(deadline: IdleDeadline): void {
    while (this.nextUnitOfWork !== null && (deadline.timeRemaining() > 0 || deadline.didTimeout)) {
      this.nextUnitOfWork = this.performUnitOfWork(this.nextUnitOfWork);
    }

    if (this.nextUnitOfWork === null) {
      // All work complete, commit to DOM
      this.completeRoot();
    } else {
      // More work to do, schedule continuation
      requestIdleCallback((deadline) => this.workLoop(deadline));
    }
  }

  // Process one fiber node
  private performUnitOfWork(fiber: FiberNode): FiberNode | null {
    // Phase 1: Begin work (traverse down)
    const next = this.beginWork(fiber);

    if (next !== null) {
      // Continue with child
      return next;
    }

    // No children, complete this fiber
    let completedWork: FiberNode | null = fiber;

    while (completedWork !== null) {
      // Phase 2: Complete work (traverse up)
      const returnFiber = completedWork.return;
      const siblingFiber = completedWork.sibling;

      this.completeWork(completedWork);

      if (siblingFiber !== null) {
        // Continue with sibling
        return siblingFiber;
      }

      // Continue completing parent
      completedWork = returnFiber;
    }

    return null;
  }

  private beginWork(fiber: FiberNode): FiberNode | null {
    switch (fiber.tag) {
      case WorkTag.FunctionComponent:
        return this.updateFunctionComponent(fiber);

      case WorkTag.ClassComponent:
        return this.updateClassComponent(fiber);

      case WorkTag.HostComponent:
        return this.updateHostComponent(fiber);

      case WorkTag.SuspenseComponent:
        return this.updateSuspenseComponent(fiber);

      default:
        return null;
    }
  }

  private updateFunctionComponent(fiber: FiberNode): FiberNode | null {
    const Component = fiber.type;
    const props = fiber.pendingProps;

    // Call the function component
    const children = Component(props);

    // Reconcile children
    return this.reconcileChildren(fiber, children);
  }

  private completeWork(fiber: FiberNode): void {
    // Mark effects that need to be applied
    if (fiber.flags & Flags.Update) {
      this.markUpdate(fiber);
    }

    if (fiber.flags & Flags.Placement) {
      this.markPlacement(fiber);
    }

    if (fiber.flags & Flags.Deletion) {
      this.markDeletion(fiber);
    }

    // Bubble up effects to parent
    if (fiber.return !== null) {
      fiber.return.flags |= fiber.flags;
      fiber.return.subtreeFlags |= fiber.subtreeFlags;
    }
  }

  // Commit phase - apply all effects to DOM
  private completeRoot(): void {
    const finishedWork = this.workInProgressRoot;
    if (finishedWork === null) return;

    // Commit all effects
    this.commitRoot(finishedWork);

    // Reset for next update
    this.workInProgressRoot = null;
    this.workInProgress = null;
  }

  private commitRoot(root: FiberNode): void {
    // Phase 1: Before mutation (getSnapshotBeforeUpdate)
    this.commitBeforeMutationEffects(root);

    // Phase 2: Mutation (DOM updates)
    this.commitMutationEffects(root);

    // Swap the tree
    root.current = root.alternate;

    // Phase 3: Layout (componentDidMount/Update)
    this.commitLayoutEffects(root);
  }
}

Priority and Scheduling

Fiber introduces priority-based scheduling to ensure high-priority updates (like user input) aren’t blocked by low-priority work:

// Priority levels in React
enum Lane {
  NoLane = 0b0000000000000000000000000000000,
  SyncLane = 0b0000000000000000000000000000001,
  InputContinuousLane = 0b0000000000000000000000000000100,
  DefaultLane = 0b0000000000000000000000000010000,
  TransitionLane1 = 0b0000000000000000000000001000000,
  TransitionLane2 = 0b0000000000000000000000010000000,
  IdleLane = 0b0100000000000000000000000000000,
  OffscreenLane = 0b1000000000000000000000000000000,
}

// Priority scheduler
class PriorityScheduler {
  private taskQueue: Task[] = [];
  private timerQueue: Task[] = [];
  private currentTask: Task | null = null;
  private isPerformingWork = false;

  scheduleCallback(priority: Priority, callback: () => void, options?: { delay?: number }): Task {
    const currentTime = performance.now();
    const startTime = currentTime + (options?.delay || 0);

    // Calculate timeout based on priority
    const timeout = this.timeoutForPriority(priority);
    const expirationTime = startTime + timeout;

    const task: Task = {
      id: this.nextTaskId++,
      callback,
      priority,
      startTime,
      expirationTime,
      sortIndex: -1,
    };

    if (startTime > currentTime) {
      // Delayed task
      task.sortIndex = startTime;
      this.push(this.timerQueue, task);

      if (this.taskQueue.length === 0 && task === this.peek(this.timerQueue)) {
        this.requestHostTimeout(this.handleTimeout, startTime - currentTime);
      }
    } else {
      // Immediate task
      task.sortIndex = expirationTime;
      this.push(this.taskQueue, task);

      if (!this.isPerformingWork) {
        this.isPerformingWork = true;
        this.requestHostCallback(this.flushWork);
      }
    }

    return task;
  }

  private timeoutForPriority(priority: Priority): number {
    switch (priority) {
      case Priority.Immediate:
        return -1; // IMMEDIATE_PRIORITY_TIMEOUT
      case Priority.UserBlocking:
        return 250; // USER_BLOCKING_PRIORITY_TIMEOUT
      case Priority.Normal:
        return 5000; // NORMAL_PRIORITY_TIMEOUT
      case Priority.Low:
        return 10000; // LOW_PRIORITY_TIMEOUT
      case Priority.Idle:
        return 1073741823; // IDLE_PRIORITY_TIMEOUT (never expires)
      default:
        return 5000;
    }
  }

  private flushWork(hasTimeRemaining: boolean, initialTime: number): boolean {
    this.isPerformingWork = true;

    try {
      return this.workLoop(hasTimeRemaining, initialTime);
    } finally {
      this.currentTask = null;
      this.isPerformingWork = false;
    }
  }

  private workLoop(hasTimeRemaining: boolean, initialTime: number): boolean {
    let currentTime = initialTime;
    this.advanceTimers(currentTime);

    this.currentTask = this.peek(this.taskQueue);

    while (this.currentTask !== null) {
      if (
        this.currentTask.expirationTime > currentTime &&
        (!hasTimeRemaining || this.shouldYieldToHost())
      ) {
        // This task hasn't expired, and we've run out of time
        break;
      }

      const callback = this.currentTask.callback;

      if (typeof callback === 'function') {
        this.currentTask.callback = null;
        const continuationCallback = callback();

        if (typeof continuationCallback === 'function') {
          // Task wants to continue
          this.currentTask.callback = continuationCallback;
        } else {
          // Task complete
          if (this.currentTask === this.peek(this.taskQueue)) {
            this.pop(this.taskQueue);
          }
        }
      } else {
        this.pop(this.taskQueue);
      }

      this.currentTask = this.peek(this.taskQueue);
      currentTime = performance.now();
    }

    // Return whether there's more work
    return this.currentTask !== null;
  }

  private shouldYieldToHost(): boolean {
    const currentTime = performance.now();
    return currentTime >= this.deadline;
  }

  // Min-heap operations for priority queue
  private push(heap: Task[], task: Task): void {
    const index = heap.length;
    heap.push(task);
    this.siftUp(heap, task, index);
  }

  private peek(heap: Task[]): Task | null {
    return heap.length === 0 ? null : heap[0];
  }

  private pop(heap: Task[]): Task | null {
    if (heap.length === 0) return null;

    const first = heap[0];
    const last = heap.pop()!;

    if (last !== first) {
      heap[0] = last;
      this.siftDown(heap, last, 0);
    }

    return first;
  }
}

Concurrent Mode Features

Concurrent Mode leverages Fiber’s architecture to enable powerful features:

// Time Slicing Example
function TimeSlicingDemo() {
  const [isPending, startTransition] = useTransition();
  const [inputValue, setInputValue] = useState('');
  const [list, setList] = useState<string[]>([]);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // High priority - immediate update
    setInputValue(e.target.value);

    // Low priority - can be interrupted
    startTransition(() => {
      const newList = [];
      for (let i = 0; i < 20000; i++) {
        newList.push(e.target.value);
      }
      setList(newList);
    });
  };

  return (
    <div>
      <input value={inputValue} onChange={handleChange} />
      {isPending && <span>Updating list...</span>}
      <ul>
        {list.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

// Suspense with Fiber
function SuspenseWithFiber() {
  // Fiber can pause rendering when a promise is thrown
  return (
    <Suspense fallback={<Loading />}>
      <AsyncComponent />
    </Suspense>
  );
}

function AsyncComponent() {
  const data = use(fetchData()); // Throws promise if not ready

  // Fiber will:
  // 1. Catch the promise
  // 2. Show fallback
  // 3. Resume rendering when promise resolves
  return <div>{data}</div>;
}

How Fiber Impacts Performance

Batching and Auto-batching

// React 18+ automatic batching
function AutoBatchingExample() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  // Before React 18: 2 renders
  // React 18+: 1 render (automatic batching)
  const handleClick = () => {
    setCount((c) => c + 1);
    setFlag((f) => !f);
    // Both updates batched automatically
  };

  // Even in async code!
  const handleAsyncClick = async () => {
    const data = await fetchData();

    // Still batched in React 18+
    setCount(data.count);
    setFlag(data.flag);
  };

  // Opt out of batching when needed
  const handleImmediateUpdate = () => {
    flushSync(() => {
      setCount((c) => c + 1);
    }); // Forces immediate render

    // This runs after count is updated
    setFlag((f) => !f);
  };

  return (
    <div>
      <p>
        Count: {count}, Flag: {flag.toString()}
      </p>
      <button onClick={handleClick}>Update Both</button>
    </div>
  );
}

Optimizing for Fiber’s Algorithm

// Component structured for Fiber optimization
function FiberOptimizedComponent({ data }: { data: Item[] }) {
  // 1. Stable keys for efficient reconciliation
  const items = useMemo(
    () =>
      data.map((item) => ({
        ...item,
        key: item.id, // Stable key
      })),
    [data],
  );

  // 2. Priority-aware updates
  const [search, setSearch] = useState('');
  const [deferredSearch] = useDeferredValue(search);

  // 3. Interrupt-friendly expensive computation
  const filtered = useMemo(
    () => items.filter((item) => item.name.toLowerCase().includes(deferredSearch.toLowerCase())),
    [items, deferredSearch],
  );

  // 4. Leverage suspense for data fetching
  return (
    <div>
      <input
        value={search}
        onChange={(e) => setSearch(e.target.value)}
        placeholder="Search (high priority)"
      />

      <Suspense fallback={<Skeleton />}>
        <ItemList items={filtered} />
      </Suspense>
    </div>
  );
}

// Understanding bailout optimization
function BailoutOptimization({ value }: { value: number }) {
  console.log('Parent render');

  // Fiber can skip rendering children if props haven't changed
  return (
    <>
      <ExpensiveChild /> {/* Skipped if no props change */}
      <CheapChild value={value} />
    </>
  );
}

const ExpensiveChild = memo(() => {
  console.log('ExpensiveChild render');
  // Complex computation...
  return <div>Expensive</div>;
});

Debugging Fiber Internals

// Accessing Fiber internals (development only)
function FiberDebugger({ children }: { children: React.ReactNode }) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (process.env.NODE_ENV === 'development') {
      // Access fiber node
      const fiberNode =
        (ref.current as any)?._reactInternalFiber || (ref.current as any)?._reactInternalInstance;

      if (fiberNode) {
        console.group('Fiber Node Structure');
        console.log('Type:', fiberNode.type);
        console.log('Tag:', fiberNode.tag);
        console.log('Props:', fiberNode.memoizedProps);
        console.log('State:', fiberNode.memoizedState);
        console.log('Effects:', fiberNode.flags);
        console.log('Priority:', fiberNode.lanes);
        console.groupEnd();

        // Traverse fiber tree
        traverseFiberTree(fiberNode);
      }
    }
  }, []);

  return <div ref={ref}>{children}</div>;
}

function traverseFiberTree(fiber: any, depth = 0) {
  const indent = '  '.repeat(depth);
  console.log(`${indent}${fiber.type?.name || fiber.type || 'Unknown'}`);

  // Traverse children
  let child = fiber.child;
  while (child) {
    traverseFiberTree(child, depth + 1);
    child = child.sibling;
  }
}

// Performance marks for Fiber phases
function measureFiberPhases() {
  if (typeof window !== 'undefined' && window.performance) {
    // React adds marks during rendering
    const marks = performance.getEntriesByType('mark');
    const reactMarks = marks.filter(
      (mark) => mark.name.startsWith('') || mark.name.includes('React'),
    );

    console.table(
      reactMarks.map((mark) => ({
        name: mark.name,
        startTime: mark.startTime,
        duration: mark.duration,
      })),
    );
  }
}

Practical Fiber Optimizations

// 1. Structure components for efficient reconciliation
function EfficientList({ items }: { items: Item[] }) {
  return (
    <div>
      {items.map((item) => (
        // Stable key = efficient reconciliation
        <Item key={item.id} {...item} />
      ))}
    </div>
  );
}

// 2. Use lanes for priority
function PriorityAwareComponent() {
  const [urgent, setUrgent] = useState('');
  const [deferred, setDeferred] = useState('');

  return (
    <div>
      <input
        value={urgent}
        onChange={(e) => {
          // Sync lane - highest priority
          setUrgent(e.target.value);
        }}
      />

      <button
        onClick={() => {
          // Transition lane - lower priority
          startTransition(() => {
            setDeferred(processData(urgent));
          });
        }}
      >
        Process
      </button>
    </div>
  );
}

// 3. Minimize fiber tree depth
// ❌ Deep nesting = more fiber nodes
function DeepNesting() {
  return (
    <div>
      <div>
        <div>
          <div>
            <Content />
          </div>
        </div>
      </div>
    </div>
  );
}

// ✅ Flat structure = fewer fiber nodes
function FlatStructure() {
  return (
    <div className="container">
      <Content />
    </div>
  );
}

// 4. Leverage Suspense boundaries
function SuspenseBoundaries() {
  return (
    <div>
      {/* Each boundary can work independently */}
      <Suspense fallback={<HeaderSkeleton />}>
        <Header />
      </Suspense>

      <Suspense fallback={<ContentSkeleton />}>
        <Content />
      </Suspense>

      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />
      </Suspense>
    </div>
  );
}

Wrapping Up

The Virtual DOM and Fiber architecture are the beating heart of React’s performance story. The Virtual DOM provides the declarative programming model we love, while Fiber transforms that model into an efficient, interruptible, priority-aware rendering machine. Understanding these internals isn’t just academic—it directly informs how you structure components, manage state, and optimize performance.

The key insights: React can interrupt and resume work, different updates have different priorities, and the reconciliation algorithm rewards stable keys and shallow component trees. Write your React code with these principles in mind, and you’re working with the framework’s natural grain rather than against it.

Prerequisites

  • Solid understanding of React fundamentals
  • Knowledge of browser event loop and main thread
  • Experience with React performance optimization concepts
  • Basic understanding of computer science algorithms

Practical Examples

Architecture knowledge applied to real scenarios:

  • Large data tables - Efficient reconciliation with stable keys
  • Animation-heavy UIs - Priority scheduling for smooth interactions
  • Progressive web apps - Time-slicing for responsive loading
  • Real-time dashboards - Concurrent updates without blocking

Master Fiber’s mental model, and you’ll write React applications that aren’t just fast—they’re predictably fast, gracefully handling everything from smooth animations to massive lists without breaking a sweat.

Last modified on .