{"id":4174,"date":"2024-10-17T17:24:30","date_gmt":"2024-10-17T17:24:30","guid":{"rendered":"https:\/\/algocademy.com\/blog\/understanding-reacts-virtual-dom-a-deep-dive-into-efficient-ui-updates\/"},"modified":"2026-10-15T12:51:16","modified_gmt":"2026-10-15T12:51:16","slug":"understanding-reacts-virtual-dom-a-deep-dive-into-efficient-ui-updates","status":"publish","type":"post","link":"https:\/\/algocademy.com\/blog\/understanding-reacts-virtual-dom-a-deep-dive-into-efficient-ui-updates\/","title":{"rendered":"React Virtual DOM Explained: Diffing, Reconciliation, Keys"},"content":{"rendered":"<p><strong>React does not use a separate virtual DOM library.<\/strong> The virtual DOM is a pattern built into React itself. When you write JSX, you create plain JavaScript objects called React elements. React&#8217;s own reconciler (called Fiber since React 16) compares the elements from the last render with the new ones, and a renderer such as <code>react-dom<\/code> applies the difference to the real DOM. There is nothing to install and nothing to configure.<\/p>\n<p>Some other libraries do ship their own virtual DOM: Vue and Preact are examples. Svelte and Solid take a different route and compile templates to direct DOM updates without one.<\/p>\n<h2>What the &#8220;virtual DOM&#8221; actually is<\/h2>\n<p>React&#8217;s old documentation defined it as &#8220;a programming concept where an ideal, or virtual, representation of a UI is kept in memory and synced with the real DOM&#8221;. That same page (the legacy &#8220;Virtual DOM and Internals&#8221; FAQ) adds that it is more a pattern than a specific technology. In React the term usually refers to the React elements your components return, and React also keeps internal objects called fibers that hold extra information about the component tree.<\/p>\n<p>The current docs at react.dev describe the same thing without the term, as three steps: <strong>trigger, render, commit<\/strong>.<\/p>\n<ol>\n<li><strong>Trigger.<\/strong> A render starts on the initial render, or when a component&#8217;s state (or an ancestor&#8217;s) changes.<\/li>\n<li><strong>Render.<\/strong> React calls your components. &#8220;Rendering&#8221; here means calling your function, not touching the screen. On a re-render, React works out what changed compared with the previous render.<\/li>\n<li><strong>Commit.<\/strong> React updates the DOM. According to react.dev, it only changes DOM nodes if there is a difference between renders; if the output is the same as last time, the DOM is not touched.<\/li>\n<\/ol>\n<p>The whole point is that you describe what the UI should look like for the current state, and React figures out the DOM operations. You never write &#8220;change this text node&#8221; yourself.<\/p>\n<h2>A toy version you can run<\/h2>\n<p>This is not how React is implemented (React&#8217;s real reconciler is far more involved), but it shows the idea: elements are plain objects, and a diff produces a short list of DOM operations.<\/p>\n<pre><code class=\"language-javascript\">\/\/ A toy virtual DOM: elements are plain objects, diff() lists the DOM operations needed.\nconst h = (type, props, ...children) =&gt; ({ type, props: props || {}, children });\n\nfunction diff(oldNode, newNode, path = \"root\", ops = []) {\n  if (oldNode === undefined) { ops.push(`create ${JSON.stringify(newNode)} at ${path}`); return ops; }\n  if (newNode === undefined) { ops.push(`remove ${path}`); return ops; }\n  if (typeof oldNode === \"string\" || typeof newNode === \"string\") {\n    if (oldNode !== newNode) ops.push(`set text at ${path}: \"${oldNode}\" -&gt; \"${newNode}\"`);\n    return ops;\n  }\n  if (oldNode.type !== newNode.type) {\n    ops.push(`replace &lt;${oldNode.type}&gt; with &lt;${newNode.type}&gt; at ${path} (whole subtree rebuilt)`);\n    return ops;\n  }\n  for (const k of new Set([...Object.keys(oldNode.props), ...Object.keys(newNode.props)])) {\n    if (oldNode.props[k] !== newNode.props[k]) ops.push(`set prop ${k} at ${path}: ${oldNode.props[k]} -&gt; ${newNode.props[k]}`);\n  }\n  const n = Math.max(oldNode.children.length, newNode.children.length);\n  for (let i = 0; i &lt; n; i++) diff(oldNode.children[i], newNode.children[i], `${path}\/${newNode.type}[${i}]`, ops);\n  return ops;\n}\n\nconst view = count =&gt; h(\"div\", { class: \"counter\" }, h(\"p\", {}, `Count: ${count}`), h(\"button\", {}, \"Increment\"));\nconsole.log(diff(view(0), view(1)));\nconsole.log(diff(view(0), { ...view(0), type: \"section\" }));<\/code><\/pre>\n<p>Output:<\/p>\n<pre><code class=\"language-text\">[ 'set text at root\/div[0]\/p[0]: \"Count: 0\" -&gt; \"Count: 1\"' ]\n[ 'replace &lt;div&gt; with &lt;section&gt; at root (whole subtree rebuilt)' ]<\/code><\/pre>\n<p>Changing the count produces one text update. Changing the root element&#8217;s type replaces the whole subtree. Both match what React does.<\/p>\n<h2>The same thing in a React component<\/h2>\n<pre><code class=\"language-jsx\">import { useState } from \"react\";\n\nfunction Counter() {\n  const [count, setCount] = useState(0);\n\n  return (\n    &lt;div&gt;\n      &lt;p&gt;Count: {count}&lt;\/p&gt;\n      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;Increment&lt;\/button&gt;\n    &lt;\/div&gt;\n  );\n}<\/code><\/pre>\n<p>On a click, <code>setCount<\/code> queues a render. React calls <code>Counter<\/code> again, gets a new tree, sees that only the text inside the <code>&lt;p&gt;<\/code> differs, and updates that text node. The <code>&lt;div&gt;<\/code> and the <code>&lt;button&gt;<\/code> are left alone.<\/p>\n<h2>How the diffing works<\/h2>\n<p>Comparing two arbitrary trees exactly is expensive, so React uses a heuristic. The legacy reconciliation docs describe it as an O(n) algorithm based on two assumptions:<\/p>\n<ol>\n<li>Two elements of different types will produce different trees.<\/li>\n<li>The developer can hint which children are stable across renders with a <code>key<\/code> prop.<\/li>\n<\/ol>\n<p>In practice this means:<\/p>\n<ul>\n<li><strong>Different type at the same position:<\/strong> React tears down the old subtree and builds a new one. Every component inside loses its state. Changing <code>&lt;div&gt;<\/code> to <code>&lt;section&gt;<\/code> around a component resets that component.<\/li>\n<li><strong>Same DOM element type:<\/strong> React keeps the DOM node and updates only the attributes that changed.<\/li>\n<li><strong>Same component type:<\/strong> React keeps the instance, so its state survives, and re-renders it with the new props.<\/li>\n<li><strong>Children:<\/strong> React walks both lists of children together and generates a change wherever they differ. Keys let it match children even when their order changes.<\/li>\n<\/ul>\n<p>The state rule is the one that bites people. react.dev puts it this way: React keeps a component&#8217;s state as long as the same component is rendered at the same position in the tree. It is the position in the UI tree that matters, not the position in your JSX. That is also why you should not define a component inside another component: the inner function is new on every render, so React sees a different type each time and resets its state.<\/p>\n<h2>Keys<\/h2>\n<pre><code class=\"language-jsx\">function TodoList({ todos }) {\n  return (\n    &lt;ul&gt;\n      {todos.map(todo =&gt; (\n        &lt;li key={todo.id}&gt;{todo.text}&lt;\/li&gt;\n      ))}\n    &lt;\/ul&gt;\n  );\n}<\/code><\/pre>\n<p>The rules from react.dev:<\/p>\n<ul>\n<li>A key must be unique among siblings (not globally) and stable. Do not generate it during render.<\/li>\n<li>Take it from your data: a database ID, or an ID you create once when the item is created (a counter, <code>crypto.randomUUID()<\/code>).<\/li>\n<li>Keys are not passed to your component as a prop. If the component needs the ID, pass it separately.<\/li>\n<\/ul>\n<p>Two mistakes are common. <strong><code>key={index}<\/code><\/strong> is what React uses if you give no key at all, and react.dev warns it &#8220;often leads to subtle and confusing bugs&#8221; once items are inserted, deleted or reordered: state such as a half-typed input stays with the position instead of following the item. <strong><code>key={Math.random()}<\/code><\/strong> is worse, because keys never match between renders, so every item and its DOM are recreated each time and user input inside the items is lost.<\/p>\n<p>A key also works as a reset button. Giving a component a different key makes React treat it as a new component with fresh state, for example <code>&lt;Profile key={userId} \/&gt;<\/code> to clear a form when the user changes.<\/p>\n<h2>Is the virtual DOM faster?<\/h2>\n<p>Not by itself. Updating the DOM directly with the exact change you know is needed will always be at least as fast as computing a diff first. The virtual DOM does extra work to find the change, so it cannot beat well-written manual DOM code. What it buys you is that you do not have to write that code, track what changed, or keep UI and data in sync by hand. React&#8217;s own docs frame it that way: reconciliation is an implementation detail, and the legacy docs note that React could re-render the whole app on every action and the end result would be the same.<\/p>\n<p>The legacy docs also warn that because React relies on heuristics, performance suffers when its assumptions are not met. A list without stable keys is one example.<\/p>\n<p>The cost to watch is rendering, not DOM updates. A component that re-renders often and does expensive work in its body can make an app slow even if React then changes nothing in the DOM. Remember too that React re-renders a component&#8217;s children by default when the component re-renders.<\/p>\n<h2>Reducing unnecessary renders<\/h2>\n<ul>\n<li><strong>Measure first.<\/strong> Use the React DevTools Profiler. If nothing feels slow, leave it.<\/li>\n<li><strong><code>memo<\/code>.<\/strong> Wrapping a component in <code>memo<\/code> skips re-rendering when its props are unchanged. react.dev says it is &#8220;a performance optimization, not a guarantee&#8221;, and is only worth it when the component re-renders often with the same props and the rendering is expensive. It does nothing useful if you pass a new object or function in every render.<\/li>\n<li><strong>React Compiler.<\/strong> React Compiler 1.0 was released as stable on October 7, 2025. It is a build-time tool that adds memoization automatically, works with React 17 and up, and react.dev says that with it you typically do not need <code>React.memo<\/code> by hand. Whether it is enabled depends on your build setup, so check the <a href=\"https:\/\/react.dev\/learn\/react-compiler\">React Compiler docs<\/a>.<\/li>\n<li><strong>Class components.<\/strong> <code>shouldComponentUpdate<\/code> still exists, but react.dev recommends function components for new code, and says <code>shouldComponentUpdate<\/code> exists only as an optimization: if your component breaks without it, fix that first.<\/li>\n<li><strong>Do not mutate state.<\/strong> Create new arrays and objects (<code>[...items, newItem]<\/code>) so React and <code>memo<\/code> can compare by identity.<\/li>\n<\/ul>\n<p>The old advice to avoid inline arrow functions in JSX is mostly outdated. An inline <code>onClick={() =&gt; ...}<\/code> on a DOM element costs almost nothing. It only matters when the function is a prop to a memoized child, where a new function each render defeats <code>memo<\/code>.<\/p>\n<h2>Common misconceptions<\/h2>\n<ul>\n<li><strong>&#8220;React compares against the real DOM.&#8221;<\/strong> It compares the new element tree with the previous one (kept in its fibers), not with the browser&#8217;s DOM.<\/li>\n<li><strong>&#8220;A re-render means a DOM update.&#8221;<\/strong> A render can finish with no DOM change at all.<\/li>\n<li><strong>&#8220;The virtual DOM is why React is fast.&#8221;<\/strong> It is why React is declarative. Speed comes from rendering less and keeping updates small.<\/li>\n<li><strong>&#8220;Fiber is a different thing from the virtual DOM.&#8221;<\/strong> Fiber is the reconciliation engine introduced in React 16. The legacy FAQ says its main goal is to enable incremental rendering of the virtual DOM.<\/li>\n<li><strong>&#8220;The virtual DOM is what makes React Native work.&#8221;<\/strong> React Native works because React separates the reconciler from the renderer: <code>react-dom<\/code> targets the browser DOM and React Native targets native views.<\/li>\n<\/ul>\n<h2>Interview questions<\/h2>\n<ul>\n<li>What does React do when state changes? (Trigger, render, commit; the state update is queued and the component is called again.)<\/li>\n<li>Why do list items need keys, and why is the index a poor key for a list that can be reordered?<\/li>\n<li>Why does a component lose its state when the element above it changes type?<\/li>\n<li>Does a re-render always change the DOM? (No.)<\/li>\n<\/ul>\n<p>To build the rest of the picture, read the posts on <a href=\"https:\/\/algocademy.com\/blog\/unlocking-the-power-of-react-components-a-beginners-guide-to-building-dynamic-user-interfaces\/\">React components<\/a> and <a href=\"https:\/\/algocademy.com\/blog\/unlocking-the-power-of-react-hooks-a-comprehensive-guide-for-developers\/\">React hooks<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Which virtual DOM library React uses (none, it has its own reconciler), how diffing and keys work, and why the virtual DOM is not &#8220;always faster&#8221;.<\/p>\n","protected":false},"author":1,"featured_media":4173,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[23],"tags":[],"class_list":["post-4174","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-problem-solving"],"_links":{"self":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/4174"}],"collection":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/comments?post=4174"}],"version-history":[{"count":2,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/4174\/revisions"}],"predecessor-version":[{"id":8936,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/4174\/revisions\/8936"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media\/4173"}],"wp:attachment":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media?parent=4174"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/categories?post=4174"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/tags?post=4174"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}