# How React Works Internally: Virtual DOM, Reconciliation, and the Rendering Pipeline

# How React Works Internally: Virtual DOM, Reconciliation, and the Rendering Pipeline

> **TL;DR:** When you call `setState`, React doesn't immediately touch the browser DOM. Instead, it re-executes your component function, builds a new Virtual DOM tree, diffs it against the previous one, and then makes the minimum set of real DOM changes needed. Understanding this pipeline end-to-end is the difference between a developer who uses React and one who truly controls it.

## Introduction

Here's the question every React developer eventually confronts: *What actually happens when you call `setState`?*

Most people can answer at the surface level — "React re-renders the component." But that answer conceals an entire engine. What does re-render mean, precisely? Does React rebuild the whole page? Does it touch every DOM node? How does it know what changed? Why does moving a `key` prop cause a component to unmount and remount instead of update?

These aren't academic questions. They directly affect performance, correctness, and your ability to debug production issues. This article builds the complete mental model: from why the real DOM is expensive, through how the Virtual DOM works, through reconciliation and the diffing algorithm, all the way to the commit phase where pixels finally change on screen.

Target audience: React developers who are comfortable with hooks and component state but want to understand what React does under the hood. We will deliberately avoid Fiber scheduler internals — the goal is a clear, accurate mental model, not an implementation audit.

---

## Why React Needed a New Rendering Approach

### The Problem With Direct DOM Manipulation

Before React, the dominant model was imperative DOM manipulation — you wrote code that directly told the browser what to change. jQuery made this ergonomic, but it didn't change the fundamental mental model: find an element, change it.

This works fine for simple pages. It breaks down badly when your UI has dozens of components whose visibility, content, and state depend on each other. You'd end up writing synchronization logic by hand: "when the user object changes, find all the DOM nodes that show the username and update each of them." Bugs lived in the gaps between those updates.

### Why DOM Operations Are Expensive

The browser DOM is a live object graph that connects your HTML to the rendering engine. Every time you change it, the browser may need to perform two expensive operations:

- **Reflow (Layout):** The browser recalculates the geometry of elements — positions, sizes, how they affect each other. Changing the width of a parent can cascade through hundreds of children.
- **Repaint:** The browser re-draws the affected pixels to the screen.

Reflows are particularly brutal because they can be triggered by reading certain layout properties (`offsetHeight`, `getBoundingClientRect`) immediately after writing others — a pattern called **layout thrashing**.

> **Note:** Not every DOM change causes a full reflow. Changing `color` triggers a repaint but not a reflow. Changing `width` triggers both. The browser is smart about batching, but it can only do so much when you're making dozens of uncoordinated changes.

With a complex UI — think a live dashboard with 50 data points updating every second — direct DOM manipulation at that frequency will visibly stutter.

### React's Approach

React shifts the mental model from *imperative* ("do this to the DOM") to *declarative* ("this is what the UI should look like given this state"). You describe the desired output; React figures out the minimal mutations to get there. This requires React to maintain its own model of the UI, compare it to the previous model when state changes, and derive a patch. That model is the Virtual DOM.

---

## Understanding the Real DOM

The DOM (Document Object Model) is the browser's internal representation of your HTML as a tree of objects. Each HTML tag becomes a node. Each node is a full JavaScript object with hundreds of properties — styles, event listeners, geometry information, accessibility attributes, and more.

```js
// Accessing a real DOM node
const button = document.getElementById('submit-btn');
console.log(Object.keys(button).length); // hundreds of properties
```

Creating and modifying these objects is orders of magnitude more expensive than working with plain JavaScript objects, because the browser has to keep its rendering tree in sync with every change. This is the core performance insight that motivates the Virtual DOM.

---

## What Is the Virtual DOM?

The Virtual DOM is React's lightweight JavaScript representation of what the UI should look like. It is a plain object tree — no browser APIs, no rendering side effects. Creating and comparing these objects is fast because they're just memory operations.

Here is what a Virtual DOM node looks like (simplified):

```js
// A React element — the Virtual DOM node
{
  type: 'button',
  props: {
    className: 'btn btn-primary',
    onClick: handleClick,
    children: 'Save Changes'
  },
  key: null,
  ref: null
}
```

When your component returns JSX, Babel transforms it into `React.createElement` calls that produce exactly this kind of object tree.

```jsx
// JSX you write
function SaveButton({ onSave }) {
  return (
    <button className="btn btn-primary" onClick={onSave}>
      Save Changes
    </button>
  );
}

// What Babel compiles it to (modern JSX transform)
import { jsx as _jsx } from 'react/jsx-runtime';
function SaveButton({ onSave }) {
  return _jsx('button', {
    className: 'btn btn-primary',
    onClick: onSave,
    children: 'Save Changes'
  });
}
```

The result of calling your component function is a tree of these plain objects — the Virtual DOM tree for that render.

![Diagram comparing the Real DOM node structure to a Virtual DOM node, showing the Real DOM's hundreds of browser properties versus the Virtual DOM's lightweight plain JavaScript object with type, props, key, and children fields](https://cdn.hashnode.com/res/hashnode/image/upload/v1786274420817/10ae5b67-ec52-4072-a6be-c3ef3c3319e0.png)

### Virtual DOM vs Real DOM

| Characteristic | Real DOM | Virtual DOM |
|---|---|---|  
| Lives in | Browser memory (C++ objects) | JavaScript heap (plain objects) |
| Creation cost | High | Very low |
| Read/write cost | High (triggers layout/paint) | Negligible |
| Size | Hundreds of properties per node | ~5 properties per node |
| Directly renderable | Yes | No |
| Diffable efficiently | Impractical | Yes |

> **Warning:** The Virtual DOM is not inherently faster than the real DOM. It's a strategy to *minimize* the number of real DOM operations by doing cheap comparisons first. If you're making a single DOM change, going through the Virtual DOM adds overhead, not removes it. React's advantage appears at scale and complexity.

---

## Initial Rendering Process

When your React application first loads, here is the sequence:

1. **React calls your root component function** (e.g., `<App />`)
2. **Your function returns JSX**, which React transforms into a Virtual DOM tree
3. **React walks the Virtual DOM tree** and, for every component node, calls that component's function recursively until the entire tree consists of host elements (`div`, `span`, `button`, etc.)
4. **React generates real DOM nodes** from this complete tree
5. **React inserts those nodes** into the actual DOM at the root mount point

```jsx
// Full initial render example
import { createRoot } from 'react-dom/client';

function UserCard({ name, role }) {
  return (
    <div className="user-card">
      <h2>{name}</h2>
      <span className="role-badge">{role}</span>
    </div>
  );
}

function App() {
  return (
    <main>
      <UserCard name="Elena Vasquez" role="Engineering Lead" />
      <UserCard name="Tariq Osei" role="Product Manager" />
    </main>
  );
}

const root = createRoot(document.getElementById('root'));
root.render(<App />);
```

On the first render, React has nothing to compare against. It simply builds the DOM from the Virtual DOM tree. The interesting part starts when state changes.

---

## What Happens When State Changes?

Let's answer the original question directly. When you call `setState` (or the state setter from `useState`), React schedules a re-render. During that re-render:

1. **React re-executes your component function** with the new state values
2. **The function returns a new Virtual DOM tree**
3. **React compares the new tree to the previous tree** — this is reconciliation
4. **React computes the minimal set of real DOM changes**
5. **React applies those changes** to the real DOM in a single batch

```jsx
import { useState } from 'react';

function NotificationBanner() {
  const [message, setMessage] = useState('Welcome back!');
  const [isVisible, setIsVisible] = useState(true);

  function handleDismiss() {
    // This triggers the entire pipeline described above
    setIsVisible(false);
  }

  if (!isVisible) return null;

  return (
    <div className="banner">
      <p>{message}</p>
      <button onClick={handleDismiss}>Dismiss</button>
    </div>
  );
}
```

When `handleDismiss` runs, React doesn't immediately remove the banner from the DOM. It schedules a re-render, re-executes `NotificationBanner`, gets `null` back, and then reconciles: the previous tree had a `div.banner`, the new tree has nothing. React removes the real DOM node.

---

## Understanding Reconciliation

Reconciliation is React's process of comparing the previous Virtual DOM tree to the new one and deciding what changed. The goal is to find the minimum set of operations to transform the old real DOM into one that matches the new Virtual DOM.

In theory, comparing two arbitrary trees has O(n³) complexity — for a tree of 1000 nodes, that's a billion comparisons. React makes two pragmatic assumptions that bring this to O(n):

1. **Elements of different types produce different trees.** If the root type changes (e.g., `div` → `section`), React tears down the old subtree and builds a new one from scratch.
2. **The developer can hint at stable identity using the `key` prop.** This allows React to track which item in a list is which across renders.

![Reconciliation diagram showing the previous Virtual DOM tree on the left and the new Virtual DOM tree on the right, with color-coded nodes indicating which elements are reused, updated, or replaced](https://cdn.hashnode.com/res/hashnode/image/upload/v1786274471994/895f8aa6-0b86-4a05-bcfd-56271fcd9e5d.png)

### How React Compares Trees

React walks both trees simultaneously, node by node:

**Same type, same position:** React keeps the real DOM node and only updates changed attributes.

```jsx
// Previous render
<input type="text" className="search-input" value="react" />

// New render — only 'value' changed
<input type="text" className="search-input" value="react hooks" />

// Real DOM operation: element.value = 'react hooks'
// The DOM node itself is reused — no teardown/rebuild
```

**Different type, same position:** React unmounts the old component, destroys its DOM, and mounts the new one.

```jsx
// Previous render
<div className="container">
  <TextEditor />
</div>

// New render — 'div' became 'section'
<section className="container">
  <TextEditor />
</section>

// React destroys the entire old subtree including TextEditor's state
// This is a common source of accidental state loss
```

> **Warning:** If you conditionally render different component types in the same position, React will unmount and remount them, destroying all local state. This is usually not what you want. Use the `key` prop or conditional logic inside a single component instead.

---

## The Diffing Algorithm

The diffing algorithm is the specific implementation of reconciliation for lists and children.

When React reconciles a list of children, it matches old children to new children by position — unless `key` props are present, in which case it matches by key.

```jsx
// Without keys — matching by position
// Previous:  [<li>Alice</li>, <li>Bob</li>, <li>Carol</li>]
// New:       [<li>Zara</li>,  <li>Alice</li>, <li>Bob</li>, <li>Carol</li>]

// React sees:
//   Position 0: Alice → Zara   (update text)
//   Position 1: Bob → Alice    (update text)
//   Position 2: Carol → Bob    (update text)
//   Position 3: (new) → Carol  (insert)
// Result: 3 updates + 1 insert = 4 operations

// With keys — matching by identity
// Previous:  [<li key="alice">, <li key="bob">, <li key="carol">]
// New:       [<li key="zara">,  <li key="alice">, <li key="bob">, <li key="carol">]

// React sees:
//   key="zara":  new → insert before alice
//   key="alice": exists → reuse, move position
//   key="bob":   exists → reuse, move position
//   key="carol": exists → reuse, move position
// Result: 1 insert + repositioning = far fewer DOM mutations
```

### Common Key Mistakes

```jsx
// ❌ Using array index as key — breaks reconciliation when list reorders
function TaskList({ tasks }) {
  return (
    <ul>
      {tasks.map((task, index) => (
        <li key={index}>{task.title}</li> // index is unstable on reorder/insert
      ))}
    </ul>
  );
}

// ✅ Using stable, unique IDs
function TaskList({ tasks }) {
  return (
    <ul>
      {tasks.map((task) => (
        <li key={task.id}>{task.title}</li> // task.id is stable
      ))}
    </ul>
  );
}
```

Using array index as a key is safe only when the list is static (never reordered, never filtered, never prepended). In any other case, index keys cause React to incorrectly reuse DOM nodes and can corrupt controlled input state.

---

## Render Phase vs Commit Phase

React's update process is split into two distinct phases with a clear boundary between them.

```mermaid
flowchart TD
  A[State Change / setState] --> B[Render Phase]
  B --> C[Re-execute Component Functions]
  C --> D[Build New Virtual DOM Tree]
  D --> E[Diff Against Previous Tree]
  E --> F[Produce Effect List]
  F --> G[Commit Phase]
  G --> H[Apply DOM Mutations]
  H --> I[Run useLayoutEffect]
  I --> J[Browser Paint]
  J --> K[Run useEffect]
```

### Render Phase

The render phase is **pure and side-effect free**. React runs your component functions (and may do so multiple times in concurrent mode), produces the new Virtual DOM, runs the diffing algorithm, and builds a list of effects — the changes that need to be made to the real DOM. Nothing visible changes during this phase.

Because the render phase is pure, React can:
- Pause it and resume it later (concurrent features)
- Discard a partially-completed render if a higher-priority update arrives
- Run it multiple times for the same update

> **Note:** This is exactly why React's documentation says you must not write to external state, kick off network requests, or cause observable side effects during the render of a component body. The render can be retried.

### Commit Phase

The commit phase is **synchronous and cannot be interrupted**. React takes the effect list produced by the render phase and applies it all at once:

1. Applies DOM mutations (insertions, updates, deletions)
2. Runs `useLayoutEffect` cleanups and setups synchronously
3. Hands control to the browser, which paints the updated pixels
4. Runs `useEffect` cleanups and setups asynchronously

This separation is important: because mutations happen in one synchronous burst, the user never sees a half-updated UI.

---

## Keys and List Rendering

Keys give React stable identity for list items across renders. They must be:
- **Unique among siblings** (not globally unique)
- **Stable across renders** (same item → same key every time)
- **Predictable** (not random, not derived from render-time Math.random())

```jsx
import { useState } from 'react';

const INITIAL_NOTIFICATIONS = [
  { id: 'n1', text: 'Deployment succeeded', type: 'success' },
  { id: 'n2', text: 'Memory usage high', type: 'warning' },
  { id: 'n3', text: 'New comment on PR #42', type: 'info' },
];

function NotificationList() {
  const [notifications, setNotifications] = useState(INITIAL_NOTIFICATIONS);

  function dismissNotification(id) {
    setNotifications(prev => prev.filter(n => n.id !== id));
  }

  function addUrgentAlert() {
    const newAlert = { id: `n${Date.now()}`, text: 'Disk space critical!', type: 'error' };
    // Prepend — index-based keys would break here
    setNotifications(prev => [newAlert, ...prev]);
  }

  return (
    <div className="notification-list">
      <button onClick={addUrgentAlert}>Simulate Alert</button>
      {notifications.map((notification) => (
        // ✅ Stable ID from the data model
        <div key={notification.id} className={`notification notification--${notification.type}`}>
          <span>{notification.text}</span>
          <button onClick={() => dismissNotification(notification.id)}>✕</button>
        </div>
      ))}
    </div>
  );
}
```

When `addUrgentAlert` runs, React matches the three existing notifications by their `id` keys and simply inserts the new one at the top. With index keys, it would update the text of every existing node and create a new one at the bottom.

---

## React Performance Optimization

### Avoiding Unnecessary Re-renders

By default, when a parent component re-renders, all its children re-render too — even if their props didn't change. React still runs the diffing algorithm and usually decides nothing changed, so no real DOM updates happen. But the component function execution itself costs time.

### React.memo

`React.memo` is a higher-order component that memoizes the rendered output of a component. React skips re-executing the component function if its props haven't changed (shallow comparison).

```jsx
import { memo, useState } from 'react';

// This component only re-renders if 'user' prop changes
const UserProfile = memo(function UserProfile({ user }) {
  console.log('UserProfile rendered');
  return (
    <div className="profile-card">
      <img src={user.avatarUrl} alt={user.name} />
      <h3>{user.name}</h3>
      <p>{user.bio}</p>
    </div>
  );
});

function Dashboard() {
  const [tickCount, setTickCount] = useState(0);
  const user = { name: 'Elena Vasquez', bio: 'Engineering Lead', avatarUrl: '/avatars/elena.jpg' };

  return (
    <div>
      {/* Updating tickCount re-renders Dashboard but NOT UserProfile */}
      <button onClick={() => setTickCount(c => c + 1)}>Tick: {tickCount}</button>
      <UserProfile user={user} />
    </div>
  );
}
```

> **Warning:** `React.memo` uses shallow comparison. If you pass a new object or array literal in props on every render (e.g., `user={{ name: 'Elena' }}`), memo will always see a different prop and never skip the render. The object reference changes even if the content is identical.

### useMemo and useCallback

`useMemo` memoizes a computed value. `useCallback` memoizes a function reference. Both are tools for stabilizing references so that `React.memo` and hooks with dependency arrays work correctly.

```jsx
import { useState, useMemo, useCallback, memo } from 'react';

const ExpensiveChart = memo(function ExpensiveChart({ data, onDataPointClick }) {
  // Imagine a complex D3 or Canvas rendering here
  return <div>Chart with {data.length} points</div>;
});

function AnalyticsDashboard({ rawMetrics }) {
  const [filter, setFilter] = useState('all');
  const [selectedPoint, setSelectedPoint] = useState(null);

  // useMemo: only recompute when rawMetrics or filter changes
  const filteredData = useMemo(() => {
    if (filter === 'all') return rawMetrics;
    return rawMetrics.filter(m => m.category === filter);
  }, [rawMetrics, filter]);

  // useCallback: stable reference so ExpensiveChart doesn't re-render on every Dashboard render
  const handleDataPointClick = useCallback((point) => {
    setSelectedPoint(point);
  }, []); // no dependencies — function doesn't close over changing state

  return (
    <div>
      <select value={filter} onChange={e => setFilter(e.target.value)}>
        <option value="all">All</option>
        <option value="errors">Errors</option>
        <option value="latency">Latency</option>
      </select>
      <ExpensiveChart data={filteredData} onDataPointClick={handleDataPointClick} />
      {selectedPoint && <PointDetail point={selectedPoint} />}
    </div>
  );
}
```

> **Tip:** Don't reach for `useMemo` and `useCallback` everywhere. They add code complexity and their own overhead. Profile first. Optimize the components where re-renders are measurably expensive.

---

## Common Virtual DOM Misconceptions

| Misconception | Reality |
|---|---|
| Virtual DOM eliminates real DOM updates | No. React still updates the real DOM — it just minimizes *how many* changes it makes |
| Virtual DOM is always faster than direct DOM manipulation | No. For a single targeted update, direct manipulation is faster. React wins at scale |
| Re-rendering means the DOM is rebuilt | No. Re-render means the component function is re-executed. DOM changes only happen in the commit phase if something actually changed |
| Reconciliation is perfectly optimal | No. The heuristic O(n) algorithm can miss some optimizations that a smarter but slower algorithm would find |
| Memoization is always the answer for slow React apps | No. Often the root cause is unnecessary state at the wrong level, not missing memo calls |

---

## The Complete React Rendering Lifecycle

Here is the full pipeline from a state change to pixels on screen:

![Complete React rendering lifecycle pipeline showing all stages from state change through virtual DOM, reconciliation, commit phase, and browser paint in a linear horizontal flow](https://cdn.hashnode.com/res/hashnode/image/upload/v1786274519340/b4b2c40b-3719-4f4c-a385-b0d9afc2a398.png)

```mermaid
flowchart LR
  A([State Change]) --> B[Component Re-executes]
  B --> C[New Virtual DOM Tree]
  C --> D{Reconciliation}
  D --> E[Diffing Algorithm]
  E --> F[Effect List]
  F --> G[Commit Phase]
  G --> H[DOM Mutations]
  H --> I[useLayoutEffect]
  I --> J([Browser Paint])
  J --> K[useEffect]
```

Each step feeds directly into the next. The render phase (B through F) is pure and interruptible. The commit phase (G through I) is synchronous and uninterruptible. The browser paint happens between `useLayoutEffect` and `useEffect`, which is why `useLayoutEffect` is the right hook when you need to read layout information before the user sees the update.

---

## Trade-offs and Pitfalls

| Scenario | Pitfall | Fix |
|---|---|---|
| Large, flat lists (1000+ items) | Even Virtual DOM diffing + render cost adds up | Virtualize with `react-window` or `react-virtual` |
| Deeply nested state updates | Every ancestor re-renders by default | Lift only necessary state; use context selectively |
| Inline object/array props | Breaks `React.memo` because reference always changes | Extract to a variable outside JSX or use `useMemo` |
| Using array index as key | Corrupt state when list is mutated | Use stable data-model IDs |
| Effects with missing dependencies | Stale closure bugs | Follow the exhaustive-deps ESLint rule |
| Changing component type in same position | Destroys child state unexpectedly | Keep the type stable; change props instead |

---

## Best Practices

1. **Structure state to minimize the blast radius of updates.** State low in the tree means fewer components re-render when it changes.

2. **Profile before optimizing.** React DevTools Profiler shows exactly which components re-rendered and why. Optimize what you can measure.

3. **Use stable keys from your data model.** Database IDs, slugs, UUIDs — anything that doesn't change when the list is reordered.

4. **Understand what triggers a re-render:** state changes, prop changes, context value changes, and parent re-renders. Knowing this lets you reason about cascades.

5. **Keep component functions pure.** Don't write to external variables, call APIs, or mutate props during render. Side effects belong in `useEffect`.

6. **Don't optimize premature.** A component that re-renders in 0.5ms doesn't need `React.memo`. Wrapping it adds cognitive overhead and memo comparison cost that might actually be slower.

7. **Use `useLayoutEffect` for DOM measurements.** If you need to read a layout property (like `offsetHeight`) and then synchronously update state before paint, `useLayoutEffect` is the correct tool.

---

## Conclusion

React is best understood as a **UI engine**, not just a component library. When you call `setState`, you're not telling React what to do to the DOM — you're declaring what your UI should look like given new state, and letting the engine figure out the minimal path to get there.

The key takeaways:

- The Virtual DOM is a cheap JavaScript representation of your UI. Comparing two Virtual DOM trees is far faster than comparing real DOM nodes.
- Reconciliation uses two heuristics — same element type means update, different type means replace; keys provide stable identity in lists.
- The render phase is pure and interruptible. The commit phase is synchronous and applies all real DOM changes at once.
- Re-rendering is not the same as real DOM updating. Your component function re-runs during render; the real DOM only changes in the commit phase if something actually diffed differently.
- Performance optimization starts with understanding the rendering lifecycle, not reflexively adding `useMemo` everywhere.

Your immediate next step: open React DevTools, enable the "Highlight updates when components render" setting, and interact with one of your existing apps. Watch which components light up and ask yourself why. That habit will teach you more about React's rendering model than any documentation could.

---

## Further Reading

- [React Documentation — Render and Commit](https://react.dev/learn/render-and-commit): The official explanation of the three-step rendering process, with excellent diagrams.
- [React Documentation — Preserving and Resetting State](https://react.dev/learn/preserving-and-resetting-state): Covers exactly when React destroys vs. preserves component state based on tree position.
- [React Documentation — You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect): Understanding the render cycle deeply informs when `useEffect` is and isn't the right tool.
- [Lin Clark's Fiber talk (React Conf 2017)](https://www.youtube.com/watch?v=ZCuYPiUIONs): A visual, accessible explanation of React Fiber — the reconciler architecture that enables concurrent features. The code comic format is exceptional.
- [Web.dev — Rendering Performance](https://web.dev/rendering-performance/): Deep background on browser reflows, repaints, and the compositor — the real DOM cost model that motivates everything React does.
