© Bohua Xu

React Virtual DOM

Dec 20, 2022 · 8min

Deep Dive into React Virtual DOM: Implementation and Mechanisms

Introduction

The Virtual DOM is one of React’s most distinctive features and a key factor in its performance optimization strategy. This article explores how React implements its Virtual DOM, the underlying mechanisms, and why it’s so effective at optimizing UI updates.

What is the Virtual DOM?

The Virtual DOM is a lightweight copy of the actual DOM, represented as JavaScript objects. It’s a programming concept where an ideal, or "virtual", representation of a UI is kept in memory and synced with the "real" DOM by a library such as ReactDOM.

Core Implementation

Here’s a simplified version of how React represents Virtual DOM nodes:

class VirtualNode {
  constructor(type, props, children) {
    this.type = type;        // DOM element type or component
    this.props = props;      // Element properties
    this.children = children;// Child elements
    this.key = props.key;    // Unique identifier for reconciliation
  }
}

// Example of creating a virtual DOM element
function createElement(type, props, ...children) {
  return new VirtualNode(
    type,
    props || {},
    children.map(child =>
      typeof child === 'object' ? child : createTextElement(child)
    )
  );
}

function createTextElement(text) {
  return new VirtualNode('TEXT_ELEMENT', { nodeValue: text }, []);
}
class VirtualNode {
  constructor(type, props, children) {
    this.type = type;        // DOM element type or component
    this.props = props;      // Element properties
    this.children = children;// Child elements
    this.key = props.key;    // Unique identifier for reconciliation
  }
}

// Example of creating a virtual DOM element
function createElement(type, props, ...children) {
  return new VirtualNode(
    type,
    props || {},
    children.map(child =>
      typeof child === 'object' ? child : createTextElement(child)
    )
  );
}

function createTextElement(text) {
  return new VirtualNode('TEXT_ELEMENT', { nodeValue: text }, []);
}

Diffing Algorithm

React’s diffing algorithm (Reconciliation) is what makes Virtual DOM efficient. Here’s how it works:

  1. Tree Comparison:
function diffTree(oldNode, newNode, patches = []) {
  // Node was removed
  if (!newNode) {
    patches.push({ type: 'REMOVE', oldNode });
    return patches;
  }
  
  // Node was added
  if (!oldNode) {
    patches.push({ type: 'ADD', newNode });
    return patches;
  }
  
  // Node was changed
  if (isChanged(oldNode, newNode)) {
    patches.push({ type: 'REPLACE', oldNode, newNode });
    return patches;
  }
  
  // If node wasn't changed, diff its children
  if (oldNode.children && newNode.children) {
    for (let i = 0; i < Math.max(oldNode.children.length, newNode.children.length); i++) {
      diffTree(oldNode.children[i], newNode.children[i], patches);
    }
  }
  
  return patches;
}
function diffTree(oldNode, newNode, patches = []) {
  // Node was removed
  if (!newNode) {
    patches.push({ type: 'REMOVE', oldNode });
    return patches;
  }
  
  // Node was added
  if (!oldNode) {
    patches.push({ type: 'ADD', newNode });
    return patches;
  }
  
  // Node was changed
  if (isChanged(oldNode, newNode)) {
    patches.push({ type: 'REPLACE', oldNode, newNode });
    return patches;
  }
  
  // If node wasn't changed, diff its children
  if (oldNode.children && newNode.children) {
    for (let i = 0; i < Math.max(oldNode.children.length, newNode.children.length); i++) {
      diffTree(oldNode.children[i], newNode.children[i], patches);
    }
  }
  
  return patches;
}

Key Optimizations

React employs several key optimizations in its Virtual DOM implementation:

  1. Batch Updates:
function batchUpdate(updates) {
  // Collect all updates
  const queue = [];
  updates.forEach(update => queue.push(update));
  
  // Process updates in next tick
  Promise.resolve().then(() => {
    const patches = queue.reduce((acc, update) => {
      return acc.concat(update());
    }, []);
    
    applyPatches(patches);
  });
}
function batchUpdate(updates) {
  // Collect all updates
  const queue = [];
  updates.forEach(update => queue.push(update));
  
  // Process updates in next tick
  Promise.resolve().then(() => {
    const patches = queue.reduce((acc, update) => {
      return acc.concat(update());
    }, []);
    
    applyPatches(patches);
  });
}
  1. Component-Level Updates:
class Component {
  shouldComponentUpdate(nextProps, nextState) {
    // Shallow comparison of current and next props/state
    return !shallowEqual(this.props, nextProps) ||
           !shallowEqual(this.state, nextState);
  }
}
class Component {
  shouldComponentUpdate(nextProps, nextState) {
    // Shallow comparison of current and next props/state
    return !shallowEqual(this.props, nextProps) ||
           !shallowEqual(this.state, nextState);
  }
}

Rendering Process

The rendering process involves several steps:

  1. Create Virtual DOM Tree:
function createVirtualTree(element) {
  if (typeof element === 'string') {
    return createTextElement(element);
  }
  
  const { type, props } = element;
  const children = props.children || [];
  
  return new VirtualNode(
    type,
    props,
    children.map(child => createVirtualTree(child))
  );
}
function createVirtualTree(element) {
  if (typeof element === 'string') {
    return createTextElement(element);
  }
  
  const { type, props } = element;
  const children = props.children || [];
  
  return new VirtualNode(
    type,
    props,
    children.map(child => createVirtualTree(child))
  );
}
  1. Apply Changes to Real DOM:
function applyPatches(patches) {
  patches.forEach(patch => {
    switch (patch.type) {
      case 'ADD':
        handleAdd(patch.newNode);
        break;
      case 'REMOVE':
        handleRemove(patch.oldNode);
        break;
      case 'REPLACE':
        handleReplace(patch.oldNode, patch.newNode);
        break;
      case 'UPDATE':
        handleUpdate(patch.oldNode, patch.changes);
        break;
    }
  });
}
function applyPatches(patches) {
  patches.forEach(patch => {
    switch (patch.type) {
      case 'ADD':
        handleAdd(patch.newNode);
        break;
      case 'REMOVE':
        handleRemove(patch.oldNode);
        break;
      case 'REPLACE':
        handleReplace(patch.oldNode, patch.newNode);
        break;
      case 'UPDATE':
        handleUpdate(patch.oldNode, patch.changes);
        break;
    }
  });
}

Performance Considerations

React’s Virtual DOM implementation includes several performance optimizations:

  1. Minimal DOM Manipulation: Only actual differences are applied to the real DOM
  2. Batched Updates: Multiple state changes are batched into a single update
  3. Event Delegation: Events are handled at a higher level in the DOM tree
  4. Memoization: Preventing unnecessary re-renders through React.memo and useMemo

Advanced Features

Fragments

function Fragment({ children }) {
  return children;
}
function Fragment({ children }) {
  return children;
}

Portals

function createPortal(children, container) {
  return {
    $typeof: REACT_PORTAL_TYPE,
    children,
    container,
    key: null
  };
}
function createPortal(children, container) {
  return {
    $typeof: REACT_PORTAL_TYPE,
    children,
    container,
    key: null
  };
}

Best Practices

  1. Use Keys for Lists:
function List({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.text}</li>
      ))}
    </ul>
  );
}
function List({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.text}</li>
      ))}
    </ul>
  );
}
  1. Avoid Deep Nesting:
// Good
function FlatComponent() {
  return (
    <>
      <Header />
      <Main />
      <Footer />
    </>
  );
}

// Avoid
function DeeplyNested() {
  return (
    <div>
      <div>
        <div>
          <div>
            {/* Too many nested divs */}
          </div>
        </div>
      </div>
    </div>
  );
}
// Good
function FlatComponent() {
  return (
    <>
      <Header />
      <Main />
      <Footer />
    </>
  );
}

// Avoid
function DeeplyNested() {
  return (
    <div>
      <div>
        <div>
          <div>
            {/* Too many nested divs */}
          </div>
        </div>
      </div>
    </div>
  );
}

Conclusion

React’s Virtual DOM implementation is a sophisticated system that balances performance with developer experience. Understanding its mechanisms helps developers write more efficient React applications and make better architectural decisions.

The Virtual DOM, combined with React’s reconciliation process, provides an efficient way to update the UI while maintaining good performance. As React continues to evolve, the Virtual DOM implementation may change, but its core principles of minimal DOM manipulation and efficient updates remain constant.

CC BY-NC-SA 4.0 2021-PRESENT © Bohua Xu