---
title: "04 — Rendering and Reconciliation — How React Turns JSX Into DOM"
uid: rendering-and-reconciliation
tags: ["react", "reconciliation", "virtual-dom", "roadmap:react", "rendering", "performance"]
excerpt: "Render and commit are two phases — React builds a virtual DOM tree, diffs it, then applies only the real mutations. Knowing which phase your code runs in is half of performance debugging."
date: 2026-08-13T03:27:46+0000
source: https://www.aveshina.my.id/en/blog/rendering-and-reconciliation
---

"Magic makes the screen update" was my render-pipeline model, which made every performance mystery a dead end. The machinery that finally clicked: **React's update cycle has two phases — render (compute what the UI should look like) and commit (touch the real DOM), separated by a virtual DOM diff.** [1][2] The virtual DOM isn't a performance feature by itself; it's a staging area that lets React compute the minimum set of DOM mutations before committing them.

## React is declarative

The starting point is that React follows a **declarative** approach: I describe what the UI should look like for a given state, and React takes care of turning that description into DOM updates [1]. I never write document.createElement or el.appendChild myself. Instead:

- I write components that return JSX based on their props and state.
- When state changes, React calls the components again to get a new description.
- React reconciles the new description with the previous one and applies the diff.

The contrast is with an **imperative** approach, where I'd manually find elements and mutate them. Declarative code is easier to reason about because the UI is a pure function of state — given the same state, the same UI comes out. The cost is the machinery that figures out the diff.

## The virtual DOM and reconciliation

The **virtual DOM (VDOM)** is a lightweight in-memory description of the UI — plain JavaScript objects representing elements [1]. React keeps two versions: the current tree and the one from the last commit. The process that compares them is called **reconciliation** [1][2]:

1. **Render phase.** React calls the components (and their children) and builds a new VDOM tree. This phase is pure — no DOM access, no side effects, just computing the description. React can pause, abort, or restart it.
2. **Diff.** React compares the new VDOM against the previous one, element by element, to determine what changed.
3. **Commit phase.** React applies the calculated changes to the real DOM in one batch — inserting, updating, or removing nodes.

The diff is the clever part. It's not a generic tree-diff (that's expensive); it uses heuristics — a few shortcut rules instead of a full comparison. Two rules drive it:

- **Different element types → tear down and rebuild.** If <a> becomes <div>, or <Counter /> becomes <Timer />, React destroys the old subtree and builds a new one. State is reset.
- **Same type → keep the instance, update props.** If <div className="a"> becomes <div className="b">, React updates just the changed attributes. State and component instances survive.

That second rule is exactly why the key prop matters in lists (see the notes on components) — it's how React matches repeated elements across renders when their order might change.

```figure
<svg viewBox="0 0 760 320" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="React update cycle in three columns. Left: JSX returned by components becomes a new virtual DOM tree. Middle: React diffs the new VDOM against the previous one. Right: the commit phase applies only the changed nodes to the real DOM.">
  <defs>
    <marker id="rarrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- col 1: render -->
    <text x="120" y="26" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">1 · Render (pure)</text>
    <rect x="50" y="44" width="140" height="180" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="120" y="66" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">new VDOM</text>
    <text x="120" y="86" font-size="10" font-family="ui-monospace,monospace" fill="#4338ca" text-anchor="middle">&lt;App&gt;</text>
    <text x="120" y="104" font-size="10" font-family="ui-monospace,monospace" fill="#4338ca" text-anchor="middle">  &lt;Header/&gt;</text>
    <text x="120" y="122" font-size="10" font-family="ui-monospace,monospace" fill="#4338ca" text-anchor="middle">  &lt;List&gt;</text>
    <text x="120" y="140" font-size="10" font-family="ui-monospace,monospace" fill="#4338ca" text-anchor="middle">    &lt;Row/&gt;</text>
    <text x="120" y="158" font-size="10" font-family="ui-monospace,monospace" fill="#4338ca" text-anchor="middle">    &lt;Row/&gt;</text>
    <text x="120" y="176" font-size="10" font-family="ui-monospace,monospace" fill="#4338ca" text-anchor="middle">  &lt;/List&gt;</text>
    <text x="120" y="200" font-size="9.5" fill="#64748b" text-anchor="middle" font-style="italic">no DOM · pure</text>

    <!-- col 2: diff -->
    <text x="380" y="26" font-size="12" font-weight="700" fill="#422006" text-anchor="middle">2 · Reconcile (diff)</text>
    <rect x="300" y="44" width="160" height="180" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="380" y="68" font-size="11" font-weight="700" fill="#422006" text-anchor="middle">compare trees</text>
    <text x="380" y="92" font-size="10" fill="#422006" text-anchor="middle">same type? → update props</text>
    <text x="380" y="112" font-size="10" fill="#422006" text-anchor="middle">diff type? → rebuild subtree</text>
    <text x="380" y="132" font-size="10" fill="#422006" text-anchor="middle">list? → match by key</text>
    <text x="380" y="200" font-size="9.5" fill="#64748b" text-anchor="middle" font-style="italic">computes min mutations</text>

    <!-- col 3: commit -->
    <text x="630" y="26" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">3 · Commit (real DOM)</text>
    <rect x="560" y="44" width="140" height="180" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="630" y="68" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">apply changes</text>
    <text x="630" y="92" font-size="10" fill="#052e16" text-anchor="middle">insert / update /</text>
    <text x="630" y="108" font-size="10" fill="#052e16" text-anchor="middle">remove nodes</text>
    <text x="630" y="200" font-size="9.5" fill="#64748b" text-anchor="middle" font-style="italic">then run effects</text>

    <!-- arrows -->
    <path d="M190,134 L298,134" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#rarrow)"/>
    <path d="M460,134 L558,134" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#rarrow)"/>

    <text x="244" y="126" font-size="9.5" fill="#64748b" text-anchor="middle">new tree</text>
    <text x="509" y="126" font-size="9.5" fill="#64748b" text-anchor="middle">mutations</text>

    <text x="380" y="280" font-size="11" fill="#475569" text-anchor="middle" font-style="italic">render is interruptible · commit is synchronous and touches the DOM once</text>
  </g>
</svg>
```

## Why this matters for performance

The two-phase split is the lens I use for every "why is this slow" question [3]:

- A **slow render** means a component is doing expensive work during the render phase, and that work runs on every render even when nothing relevant changed. The fix is to do less work in render (memoize with useMemo/useCallback, move expensive computation out, or React.memo the component so it skips re-rendering when props are unchanged).
- A **slow commit** means React is writing too many DOM nodes. The fix is usually to render fewer nodes, or to keep list items stable with good keys so React doesn't tear down and rebuild large subtrees.

Kent C. Dodds's framing stuck with me: **fix the slow render before you fix the re-render.** [3] Re-renders are often cheap; a re-render only matters if the render itself is expensive. Memoizing a component that renders in microseconds solves nothing.

## How I use this

When the UI feels sluggish, I first ask whether the problem is in the render phase or the commit phase. A React DevTools profiler render that takes tens of milliseconds points at the render work itself — that's where I look for expensive computation, missing memoization, or a parent re-rendering children that didn't need to. DOM thrash (lots of insertions) shows up as commit time. The mental separation keeps me from reaching for useMemo reflexively: I only memoize when a profiler run shows an actual cost, because most renders are cheap and premature memoization adds bookkeeping overhead for no gain.

## References

[1] React team, "Render and commit," react.dev, 2024. [Online]. Available: [https://react.dev/learn/render-and-commit](https://react.dev/learn/render-and-commit)

[2] ui.dev, "Rendering in React," 2023. [Online]. Available: [https://ui.dev/why-react-renders](https://ui.dev/why-react-renders)

[3] K. C. Dodds, "Fix the slow render before you fix the re-render," kentcdodds.com, 2021. [Online]. Available: [https://kentcdodds.com/blog/fix-the-slow-render-before-you-fix-the-re-render](https://kentcdodds.com/blog/fix-the-slow-render-before-you-fix-the-re-render)

[4] React team, "Preserving and resetting state," react.dev, 2024. [Online]. Available: [https://react.dev/learn/preserving-and-resetting-state](https://react.dev/learn/preserving-and-resetting-state)

```quiz
Q: React's update cycle is best described as two phases:
- render (compute the VDOM) then commit (touch the real DOM)
- render and paint, with no diffing in between
- build then deploy
correct: 0
explain: React renders components to a virtual DOM, diffs it against the previous tree, then commits only the calculated DOM mutations in a separate phase.

Q: During reconciliation, what happens when an element changes type (e.g. <a> → <div>)?
- React updates just the changed attributes and keeps the instance
- React tears down the old subtree and builds a new one, resetting state
- React throws an error
correct: 1
explain: Different element types are treated as different elements; React unmounts the old subtree and mounts a new one, so any state is reset.

Q: Where does a component's render-phase work run?
- In the browser's paint step
- In pure computation — no DOM access — and React may interrupt or restart it
- Inside the commit phase, directly mutating the DOM
correct: 1
explain: The render phase is pure and side-effect-free; React can pause, abort, or rerun it. DOM mutations happen only in the commit phase.

Q: "Fix the slow render before you fix the re-render" means…
- stop re-renders at all costs
- a re-render only matters if the render itself is expensive; profile first
- always wrap every component in React.memo
correct: 1
explain: Most re-renders are cheap. The cost comes from expensive render work, so profile and address that before chasing down every unnecessary re-render with memoization.

Q: The `key` prop in lists exists mainly to…
- make elements render faster
- help reconciliation match repeated elements across renders when order changes
- satisfy the JSX compiler
correct: 1
explain: Keys give React a stable identity for list items so it can match them across renders. Index-based keys break when items reorder, leaking state between items.
```
