<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Deepmalya Mallick</title>
    <description>The latest articles on DEV Community by Deepmalya Mallick (@deep007).</description>
    <link>https://dev.to/deep007</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3764777%2F20fca39b-08e3-45d9-9ed8-e8fef0f95062.png</url>
      <title>DEV Community: Deepmalya Mallick</title>
      <link>https://dev.to/deep007</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/deep007"/>
    <language>en</language>
    <item>
      <title>Building a Virtual DOM from Scratch: What I Learned Reverse-Engineering React</title>
      <dc:creator>Deepmalya Mallick</dc:creator>
      <pubDate>Thu, 12 Feb 2026 06:36:02 +0000</pubDate>
      <link>https://dev.to/deep007/building-a-virtual-dom-from-scratch-what-i-learned-reverse-engineering-react-44gc</link>
      <guid>https://dev.to/deep007/building-a-virtual-dom-from-scratch-what-i-learned-reverse-engineering-react-44gc</guid>
      <description>&lt;p&gt;I used React for a long time before I realized something slightly embarrassing:&lt;br&gt;
I couldn’t actually explain how the Virtual DOM worked.&lt;br&gt;
I knew the rules everyone repeats:&lt;/p&gt;

&lt;p&gt;"Don't mutate state"&lt;br&gt;
"Add keys to your lists"&lt;br&gt;
"React diffs the virtual DOM"&lt;/p&gt;

&lt;p&gt;But if you asked me what actually happens when setState() runs? I'd fumble through a vague explanation about "reconciliation" and hope you didn't ask follow-up questions.&lt;br&gt;
So I did what any student with too much time during semester break would do: I stopped reading blog posts and built my own Virtual DOM from scratch.&lt;br&gt;
That experiment became Weave — a complete VDOM reconciler written in TypeScript with keyed reconciliation, lifecycle hooks, immutable VNodes, and a live metrics dashboard that visualizes its own internal operations.&lt;br&gt;
This isn't a tutorial. It's what I learned by actually building the thing.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Problem: Why Virtual DOM Even Exists
&lt;/h2&gt;

&lt;p&gt;Let me show you the most naive way to update the DOM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function updateCounter(count) {
  document.body.innerHTML = `
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;Counter: ${count}&amp;lt;/h1&amp;gt;
      &amp;lt;button onclick="updateCounter(${count + 1})"&amp;gt;+1&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  `;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This works. For exactly one click.&lt;br&gt;
Then you notice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The entire DOM tree gets destroyed&lt;/li&gt;
&lt;li&gt;Every element gets recreated from scratch&lt;/li&gt;
&lt;li&gt;All event listeners re-register&lt;/li&gt;
&lt;li&gt;You lose input focus, scroll position, everything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a toy counter? Maybe acceptable.&lt;br&gt;
For a real app with hundreds of elements? Completely unusable.&lt;br&gt;
The Virtual DOM solves this by adding a middle layer:&lt;/p&gt;

&lt;p&gt;Describe what you want as plain JavaScript objects (VNodes)&lt;br&gt;
Compare the old description vs the new one&lt;br&gt;
Generate minimal change instructions (patches)&lt;br&gt;
Apply only those specific changes to the real DOM&lt;/p&gt;

&lt;p&gt;Instead of rebuilding everything, you reuse what's already there and update only what changed.&lt;br&gt;
The real challenge isn't applying changes efficiently. It's figuring out what changed in the first place.&lt;/p&gt;


&lt;h2&gt;
  
  
  Architecture: The One Rule I Refused to Break
&lt;/h2&gt;

&lt;p&gt;Before I wrote any code, I made one non-negotiable decision:&lt;br&gt;
The diffing algorithm cannot know anything about the DOM.&lt;br&gt;
Why? Because comparing two tree structures is pure logic. It shouldn't matter if you're rendering to:&lt;/p&gt;

&lt;p&gt;Browser DOM&lt;br&gt;
Canvas&lt;br&gt;
Terminal UI&lt;br&gt;
React Native&lt;br&gt;
Or something completely different&lt;/p&gt;

&lt;p&gt;That led me to this layered architecture:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────────────┐
│           USER CODE           │
│   h('div', props, children)   │
└───────────────────────────────┘
                │
                ▼
┌───────────────────────────────┐
│              CORE             │
│   (Platform-Agnostic Logic)   │
│                               │
│   • VNode creation (immutable)│
│   • __id identity assignment  │
│   • diff(old, new)            │
└───────────────────────────────┘
                │
                ▼
        ┌─────────────────┐
        │     Patch[]     │
        │  (Declarative)  │
        └─────────────────┘
                │
                ▼
┌───────────────────────────────┐
│           RENDERER            │
│                               │
│   • Applies patches           │
│   • Preserves DOM identity    │
│   • Runs lifecycle hooks      │
│   • Tracks metrics            │
└───────────────────────────────┘
                │
                ▼
┌───────────────────────────────┐
│         DOM HOST              │
│      (Platform Layer)         │
│                               │
│   createElement()             │
│   setProp()                   │
│   insert() / remove()         │
└───────────────────────────────┘
                │
                ▼
┌───────────────────────────────┐
│           REAL DOM            │
│   &amp;lt;div&amp;gt;&amp;lt;h1&amp;gt;Hello&amp;lt;/h1&amp;gt;&amp;lt;/div&amp;gt;   │
└───────────────────────────────┘

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Let me break down what each layer does:&lt;/p&gt;
&lt;h2&gt;
  
  
  Core Layer (Platform-Agnostic)
&lt;/h2&gt;

&lt;p&gt;This is where the actual diffing happens. It knows nothing about browsers, DOM nodes, or document.createElement().&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From: src/core/types.ts
export interface VNode {
  readonly type: VNodeType;      // 'div', 'span', etc.
  readonly props: VNodeProps;     // { class: 'box', onClick: ... }
  readonly children: VNodeChildren; // 'text' or [VNode, VNode]
  readonly key: VNodeKey;         // For list reconciliation
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A VNode is just data. No methods, no hidden behavior.&lt;br&gt;
The diff() function takes two trees and returns declarative patches:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From: src/core/diff.ts
export function diff(
  oldVNode: VNode | null,
  newVNode: VNode | null
): Patch[] {
  // Returns instructions like:
  // [
  //   { type: 'UPDATE_TEXT', vnode: ..., value: 'New text' },
  //   { type: 'SET_PROP', vnode: ..., key: 'class', value: 'active' }
  // ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Renderer Layer (Platform Bridge)
&lt;/h2&gt;

&lt;p&gt;The renderer takes those patches and applies them. It maintains a map of VNode IDs to real DOM nodes:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From: src/renderer/createRenderer.ts
const nodeMap = new Map&amp;lt;number, Node&amp;gt;();

function commit(patches: Patch[]): void {
  for (const patch of patches) {
    switch (patch.type) {
      case 'UPDATE_TEXT': {
        const node = nodeMap.get(patch.vnode.__id);
        node.textContent = patch.value;
        break;
      }
      // ... handle other patch types
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Platform Layer (DOM-Specific)
&lt;/h2&gt;

&lt;p&gt;This is the only part that actually touches the browser DOM:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From: src/platforms/dom/host.ts
export const domHost: HostConfig&amp;lt;DomNode&amp;gt; = {
  createElement(type: string): HTMLElement {
    return document.createElement(type);
  },

  setProp(node: DomNode, key: string, value: unknown): void {
    if (key.startsWith('on') &amp;amp;&amp;amp; typeof value === 'function') {
      const eventName = key.slice(2).toLowerCase();
      node.addEventListener(eventName, value as EventListener);
    } else {
      node.setAttribute(key, String(value));
    }
  },

  insert(parent: DomNode, child: DomNode, index: number): void {
    const refNode = parent.childNodes[index] ?? null;
    parent.insertBefore(child, refNode);
  }
  // ... more methods
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Why this matters: If I want to render to Canvas tomorrow, I only swap out the platform layer. The core diffing algorithm stays exactly the same.&lt;br&gt;
I even enforced this separation with ESLint:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From: eslint.config.cjs
'import/no-restricted-paths': [
  'error',
  {
    zones: [
      {
        target: './src/core',
        from: ['./src/renderer', './src/platforms'],
        message: 'core must remain platform-agnostic'
      }
    ]
  }
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If I accidentally try to import DOM code into the core? Build fails. The architecture boundary is enforced at compile time.&lt;/p&gt;


&lt;h2&gt;
  
  
  Immutability: The Decision That Prevented So Many Bugs
&lt;/h2&gt;

&lt;p&gt;Every VNode in Weave is completely frozen:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From: src/core/vnode.ts
export function createVNode(
  type: VNodeType,
  props: VNodeProps,
  children: VNodeChildren,
  key: VNodeKey = null
): VNode {
  const vnode: VNode = { type, props, children, key };

  // Freeze everything
  Object.freeze(vnode);
  if (props) Object.freeze(props);
  if (Array.isArray(children)) Object.freeze(children);

  return vnode;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;At first, this felt unnecessary. "Who would mutate a VNode anyway?"&lt;br&gt;
Turns out, accidentally mutating VNodes is extremely easy when you're debugging and testing different approaches. And when it happens, the bugs are subtle and confusing.&lt;br&gt;
Freezing gives you:&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Guaranteed immutability
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const vnode = h('div', null, 'Hello');
vnode.children = 'World'; // ❌ TypeError in strict mode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  2. Fast reference equality checks
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (oldVNode === newVNode) {
  // Guaranteed to be identical - skip diffing entirely
  return [];
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  3. Easier debugging
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;VNodes can't change after creation&lt;/li&gt;
&lt;li&gt;No "who modified this?" mysteries&lt;/li&gt;
&lt;li&gt;Data flow is predictable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is slightly higher memory usage (more objects created), but it's absolutely worth it.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Internal __id System: Solving DOM Node Identity
&lt;/h2&gt;

&lt;p&gt;Here's a problem I didn't anticipate early on:&lt;br&gt;
How do you know which DOM node to reuse when the VNode tree changes?&lt;br&gt;
Consider this update:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Before
const before = h('div', null, [
  h('p', { key: 'a' }, 'Hello'),
  h('p', { key: 'b' }, 'World')
]);

// After  
const after = h('div', null, [
  h('p', { key: 'b' }, 'World'),  // moved up
  h('p', { key: 'a' }, 'Goodbye') // moved down, text changed
]);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The keys tell us semantic identity ('a' is still 'a', 'b' is still 'b').&lt;br&gt;
But when the renderer processes this, how does it know which actual &lt;/p&gt;
&lt;p&gt; element in the DOM corresponds to which VNode?&lt;/p&gt;
&lt;h2&gt;
  
  
  Solution: Internal stable IDs
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From: src/core/vnode.ts
let vnodeId = 0;

export function createVNode(...): VNode {
  const vnode: VNode = { type, props, children, key };

  // Attach non-enumerable __id
  Object.defineProperty(vnode, '__id', {
    value: vnodeId++,
    enumerable: false,  // Won't show in console.log or JSON.stringify
    writable: false,
    configurable: false
  });

  Object.freeze(vnode);
  return vnode;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now the renderer maintains a simple map:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From: src/renderer/createRenderer.ts
const nodeMap = new Map&amp;lt;number, Node&amp;gt;();

function createNode(vnode: VNode): Node {
  const node = host.createElement(vnode.type);
  nodeMap.set(vnode.__id, node);  // Store the mapping
  return node;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;When a MOVE patch happens, we can instantly look up the real DOM node and reposition it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;case 'MOVE': {
  const parentNode = nodeMap.get(patch.parent.__id);
  const childNode = nodeMap.get(patch.vnode.__id);

  // Reuse existing DOM node - don't recreate
  host.remove(childNode);
  host.insert(parentNode, childNode, patch.to);
  break;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is when keys finally clicked for me. They're not about micro-optimizations. They're about preserving identity so the renderer knows what to reuse.&lt;/p&gt;


&lt;h2&gt;
  
  
  Patch-Based Updates: The Core Insight
&lt;/h2&gt;

&lt;p&gt;The diffing algorithm never touches the DOM. It just returns declarative patches:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From: src/core/patch-types.ts
export type Patch =
  | ReplacePatch      // Replace entire subtree
  | UpdateTextPatch   // Update text content in place
  | InsertPatch       // Add new child at index
  | RemovePatch       // Remove child
  | SetPropPatch      // Set/update property  
  | RemovePropPatch   // Remove property
  | MovePatch         // Reorder child (keyed lists)
  | UpdatePatch;      // Trigger lifecycle hooks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For example, changing text from "Hello" to "World" produces:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[
  {
    type: 'UPDATE_TEXT',
    vnode: previousVNode,
    value: 'World'
  },
  {
    type: 'UPDATE',
    oldVNode: previousVNode,
    newVNode: currentVNode
  }
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This separation gives you:&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Pure testability
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const patches = diff(oldTree, newTree);
expect(patches).toEqual([
  { type: 'UPDATE_TEXT', value: 'World' }
]);
// No DOM needed!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  2. Batching opportunities
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Collect patches from multiple updates&lt;/li&gt;
&lt;li&gt;Apply them all at once&lt;/li&gt;
&lt;li&gt;Minimize layout thrashing&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  3. Platform flexibility
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Same patches work for DOM, Canvas, Terminal&lt;/li&gt;
&lt;li&gt;Just swap the renderer&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Keyed vs Non-Keyed Reconciliation: The Algorithm That Made It Click
&lt;/h2&gt;

&lt;p&gt;This is where it got really interesting.&lt;/p&gt;
&lt;h2&gt;
  
  
  Non-Keyed (Simple Index-Based)
&lt;/h2&gt;

&lt;p&gt;Without keys, diffing is straightforward but inefficient:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From: src/core/diff.ts (non-keyed path)
for (let i = 0; i &amp;lt; max; i++) {
  const oldChild = oldChildren[i] ?? null;
  const newChild = newChildren[i] ?? null;

  if (oldChild === null &amp;amp;&amp;amp; newChild !== null) {
    patches.push({ type: 'INSERT', vnode: newChild, index: i });
  }
  else if (oldChild !== null &amp;amp;&amp;amp; newChild === null) {
    patches.push({ type: 'REMOVE', vnode: oldChild });
  }
  else if (oldChild &amp;amp;&amp;amp; newChild) {
    patches.push(...diffNonNull(oldChild, newChild));
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Problem: Reordering causes unnecessary updates:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Before: ['A', 'B', 'C']
// After:  ['C', 'A', 'B']

// Without keys, index-based diff sees:
// Position 0: 'A' → 'C' (UPDATE_TEXT)
// Position 1: 'B' → 'A' (UPDATE_TEXT)  
// Position 2: 'C' → 'B' (UPDATE_TEXT)
// Result: All 3 nodes get their text updated!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Keyed (Identity-Preserving)
&lt;/h2&gt;

&lt;p&gt;With keys, we build a map and track which nodes moved:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From: src/core/diff.ts (keyed path)
if (hasKeys) {
  // Build lookup map: key → VNode
  const oldKeyMap = new Map&amp;lt;string | number, VNode&amp;gt;();
  oldChildren.forEach(child =&amp;gt; {
    if (child.key != null) {
      oldKeyMap.set(child.key, child);
    }
  });

  const usedOld = new Set&amp;lt;VNode&amp;gt;();

  newChildren.forEach((newChild, newIndex) =&amp;gt; {
    const key = newChild.key;

    if (key != null &amp;amp;&amp;amp; oldKeyMap.has(key)) {
      const oldChild = oldKeyMap.get(key)!;
      usedOld.add(oldChild);

      // Found matching key - reuse the node
      patches.push(...diffNonNull(oldChild, newChild));

      // Check if it moved
      const oldIndex = oldIndexMap.get(oldChild)!;
      if (oldIndex !== newIndex) {
        patches.push({
          type: 'MOVE',
          vnode: oldChild,
          from: oldIndex,
          to: newIndex
        });
      }
    } else {
      // New node - insert it
      patches.push({
        type: 'INSERT',
        vnode: newChild,
        index: newIndex
      });
    }
  });

  // Remove unused old nodes
  oldChildren.forEach(oldChild =&amp;gt; {
    if (!usedOld.has(oldChild)) {
      patches.push({ type: 'REMOVE', vnode: oldChild });
    }
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With keys:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Before: [A(key='a'), B(key='b'), C(key='c')]
// After:  [C(key='c'), A(key='a'), B(key='b')]

// Keyed diff produces:
// - MOVE C from index 2 → 0
// - MOVE A from index 0 → 1
// - MOVE B from index 1 → 2
// Result: All nodes reused, just repositioned. Zero text updates!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Performance impact:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Without keys: O(n) DOM updates (text changes)&lt;/li&gt;
&lt;li&gt;With keys: O(n) MOVE operations, zero DOM recreation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why React complains about missing keys. It's not nitpicking — it's the difference between recreating elements and simply moving them.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Hard Parts (Where I Actually Broke Things)
&lt;/h2&gt;
&lt;h2&gt;
  
  
  Challenge 1: DOM Nodes Can't "Move"
&lt;/h2&gt;

&lt;p&gt;This bug took me embarrassingly long to figure out.&lt;br&gt;
DOM nodes don't have a "move" operation. You can only remove and re-insert:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From: src/renderer/createRenderer.ts
case 'MOVE': {
  const parentNode = nodeMap.get(patch.parent.__id);
  const childNode = nodeMap.get(patch.vnode.__id);

  // ❌ WRONG - trying to be clever
  // host.insert(parentNode, childNode, patch.to);

  // ✅ CORRECT - explicit remove first
  host.remove(childNode);
  host.insert(parentNode, childNode, patch.to);
  break;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Why? Because insertBefore() has a side effect: if the node already exists in the parent, it gets moved. But if you're trying to swap adjacent nodes, this breaks:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Trying to swap [A, B] → [B, A]
// Naive approach:
// 1. Insert B at index 0 → [B, A, B] (B duplicates!)
// 2. Insert A at index 1 → [B, A, B, A] (Both duplicate!)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The fix: Always remove first. It's more explicit and actually works correctly.&lt;/p&gt;


&lt;h2&gt;
  
  
  Challenge 2: Async Removal Hooks
&lt;/h2&gt;

&lt;p&gt;I wanted to support exit animations, so I added lifecycle hooks:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From: src/core/types.ts
export interface VNodeHooks&amp;lt;Node = unknown&amp;gt; {
  create?: (vnode: VNode, node: Node) =&amp;gt; void;
  update?: (oldVNode: VNode, newVNode: VNode, node: Node) =&amp;gt; void;
  remove?: (vnode: VNode, node: Node, done: () =&amp;gt; void) =&amp;gt; void;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The remove hook gets a done callback that controls when removal actually happens:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From: src/renderer/createRenderer.ts
case 'REMOVE': {
  const node = nodeMap.get(patch.vnode.__id);
  const removeHook = patch.vnode.props?.hooks?.remove;

  const finalizeRemoval = () =&amp;gt; {
    host.remove(node);
    metrics.nodes.removed++;
    nodeMap.delete(patch.vnode.__id);
  };

  if (removeHook) {
    // Let the hook control timing
    removeHook(patch.vnode, node, finalizeRemoval);
  } else {
    // No hook - remove immediately
    finalizeRemoval();
  }
  break;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Usage:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;h('div', {
  hooks: {
    remove(vnode, node, done) {
      // Fade out over 300ms
      node.style.transition = 'opacity 300ms';
      node.style.opacity = '0';

      setTimeout(done, 300); // Call done() when animation finishes
    }
  }
}, 'Goodbye!')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The element stays in the DOM during the animation, then gets removed when done() is called.&lt;/p&gt;


&lt;h2&gt;
  
  
  Challenge 3: Keeping the Identity Map in Sync
&lt;/h2&gt;

&lt;p&gt;This was a subtle bug that caused mysterious issues.&lt;br&gt;
When you update a VNode, it gets a new internal ID:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const vnode1 = h('div', null, 'Hello'); // __id: 0
const vnode2 = h('div', null, 'World'); // __id: 1 (different ID!)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But the actual DOM node stays the same &lt;/p&gt;.&lt;br&gt;
If the renderer doesn't update its nodeMap, it keeps pointing to the old ID and future patches fail.

&lt;p&gt;The fix: Always emit an UPDATE patch:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From: src/core/diff.ts
function diffNonNull(prev: VNode, next: VNode): Patch[] {
  const patches: Patch[] = [];

  // ... all the actual diffing logic ...

  // ✅ CRITICAL: Always emit UPDATE to sync IDs
  patches.push({
    type: 'UPDATE',
    oldVNode: prev,
    newVNode: next
  });

  return patches;
}
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Then the renderer updates the mapping:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From: src/renderer/createRenderer.ts
case 'UPDATE': {
  const oldV = patch.oldVNode as VNodeWithId;
  const newV = patch.newVNode as VNodeWithId;
  const node = nodeMap.get(oldV.__id);

  if (node) {
    // Update the mapping
    nodeMap.delete(oldV.__id);
    nodeMap.set(newV.__id, node);

    // Run lifecycle hook if present
    newV.props?.hooks?.update?.(oldV, newV, node);
  }
  break;
}
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Now the map stays in sync across all updates.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Dashboard: Proof It Actually Works
&lt;/h2&gt;

&lt;p&gt;At some point I got tired of guessing if optimizations were working. So I built a live metrics dashboard using Weave itself&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jd0i0nsq82e14w4dgoc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jd0i0nsq82e14w4dgoc.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are the high-level stats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Updates (23)&lt;/strong&gt;: Total number of &lt;code&gt;root.update()&lt;/code&gt; calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avg Time (0.65ms)&lt;/strong&gt;: Average duration of each update cycle (diff + patch application)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active Nodes (1017)&lt;/strong&gt;: Current number of DOM nodes being managed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total Patches (3005)&lt;/strong&gt;: Cumulative patch operations applied since initialization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fact that average update time is sub-millisecond shows the VDOM overhead is negligible.&lt;/p&gt;
&lt;h3&gt;
  
  
  Patch Timeline (Left Middle)
&lt;/h3&gt;

&lt;p&gt;This is my favorite part. It shows recent patch operations in reverse chronological order:&lt;/p&gt;

&lt;p&gt;05:30:27.519  UPDATE   node&lt;br&gt;
05:30:27.519  UPDATE   node&lt;br&gt;
05:30:27.519  UPDATE   node&lt;br&gt;
05:30:27.519  UPDATE_TEXT  Text → "20"&lt;/p&gt;

&lt;p&gt;Each row shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Timestamp: When the patch was applied (millisecond precision)&lt;/li&gt;
&lt;li&gt;Patch Type: UPDATE, UPDATE_TEXT, INSERT, etc.&lt;/li&gt;
&lt;li&gt;Description: What changed (e.g., text content, VNode ID)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you click the +1 button, you see new UPDATE patches appear immediately. It's the VDOM watching itself work.&lt;br&gt;
Patch Operations (Right Middle)&lt;br&gt;
This heatmap shows the distribution of patch types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UPDATE (2034): Most common - fired on every node that's reused&lt;/li&gt;
&lt;li&gt;INSERT (320): New nodes added&lt;/li&gt;
&lt;li&gt;REMOVE (282): Nodes removed&lt;/li&gt;
&lt;li&gt;UPDATE_TEXT (223): Text content changes&lt;/li&gt;
&lt;li&gt;SET_PROP (145): Property updates&lt;/li&gt;
&lt;li&gt;REPLACE (1): Full subtree replacement (rare)&lt;/li&gt;
&lt;li&gt;MOVE (0): No list reorderings yet&lt;/li&gt;
&lt;li&gt;REMOVE_PROP (0): No properties removed yet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The horizontal bars give you a visual sense of what operations dominate. In this session, UPDATE patches far outnumber everything else — which is exactly what you want. It means nodes are being reused, not recreated.&lt;/p&gt;

&lt;p&gt;Update Duration Trend (Bottom Left)&lt;br&gt;
This bar chart shows performance over the last 20 updates. Each bar represents one update() call.&lt;br&gt;
You can see the durations are very consistent (all around 0.6-0.8ms) with a few spikes. Those spikes happen when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large batch updates occur&lt;/li&gt;
&lt;li&gt;Many nodes are inserted/removed at once&lt;/li&gt;
&lt;li&gt;Complex prop updates happen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a production system, you'd watch this chart for regressions. If a change causes update times to spike, you know something broke.&lt;/p&gt;
&lt;h2&gt;
  
  
  Interactive Demo (Bottom Right)
&lt;/h2&gt;

&lt;p&gt;The counter (currently at "21") with +1/-1 buttons.&lt;br&gt;
Here's the meta part: This counter is built with Weave. When you click +1:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The counter VNode updates: h('div', null, String(21)) → h('div', null, String(22))&lt;/li&gt;
&lt;li&gt;The diff algorithm generates patches&lt;/li&gt;
&lt;li&gt;The renderer applies them&lt;/li&gt;
&lt;li&gt;All of this gets recorded in the metrics&lt;/li&gt;
&lt;li&gt;The dashboard updates itself (also using Weave) to show the new patches&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's Weave observing itself in real-time.&lt;/p&gt;

&lt;p&gt;The entire dashboard is ~300 lines of code using the same h() function and update mechanism:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From: demo/dashboard/dashboard.ts
function Dashboard(metrics: RendererMetrics, counter: number): VNode {
  return h('div', { class: 'dashboard-container' }, [
    // Performance metrics
    h('div', { class: 'metrics-grid' }, [
      MetricCard('Updates', String(metrics.updates + 1), 'total'),
      MetricCard('Avg Time', metrics.avgUpdateDurationMs.toFixed(2), 'ms'),
      MetricCard('Active Nodes', String(metrics.nodes.active), 'nodes'),
      MetricCard('Total Patches', String(metrics.patches.total), 'ops')
    ]),

    // Patch timeline
    PatchTimeline(metrics.patchHistory),

    // Patch heatmap
    PatchHeatmap(metrics),

    // Update duration chart
    LiveChart(metrics.history.durations),

    // Interactive demo
    InteractiveDemo(counter, onIncrement, onDecrement)
  ]);
}

function renderDashboard(): void {
  const vnode = Dashboard(dashboardRoot.metrics, demoCounter);
  dashboardRoot.update(vnode);
}
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Watching this run made everything "click" in a way reading React source code never did.&lt;/p&gt;


&lt;h2&gt;
  
  
  What I Actually Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Immutability Isn't Academic&lt;br&gt;
Freezing objects felt wasteful when I first did it. "Why prevent mutation that won't happen?"&lt;br&gt;
Turns out, accidental mutations do happen during development. And when they do, the bugs are incredibly subtle.&lt;br&gt;
Frozen VNodes eliminated an entire class of issues I didn't even know existed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The DOM Is Always the Bottleneck&lt;br&gt;
I spent time optimizing the diff algorithm — reducing allocations, caching results, etc.&lt;br&gt;
Then I profiled it.&lt;br&gt;
VDOM overhead: 0.01-0.05ms&lt;br&gt;
Actual DOM manipulation: 0.5-2ms&lt;br&gt;
The abstraction isn't the slow part. It never was.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Separation of Concerns Is Hard But Worth It&lt;br&gt;
Keeping the core platform-agnostic required constant discipline:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Resisting shortcuts ("just import document here")&lt;br&gt;
Designing clean interfaces&lt;br&gt;
Fighting the urge to optimize for one specific platform&lt;/p&gt;

&lt;p&gt;But now:&lt;/p&gt;

&lt;p&gt;The core is fully unit-testable (no jsdom required)&lt;br&gt;
The renderer is swappable&lt;br&gt;
Adding Canvas/Terminal support is just a new host layer&lt;/p&gt;

&lt;p&gt;The architecture actually scales.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keys Aren't About Performance Micro-Optimizations
Before building this, I thought keys were about making React "go faster."
That's not it.
Keys are about preserving identity so the reconciler knows which nodes to reuse vs recreate. Without them, reordering a list destroys and recreates everything. With them, nodes just move.
It's not a speedup. It's a completely different algorithm.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Add Fragment Support from the Start&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Right now, every VNode needs a single root:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Not supported
return [
  h('li', null, 'Item 1'),
  h('li', null, 'Item 2')
];

// ✅ Must wrap
return h('ul', null, [
  h('li', null, 'Item 1'),
  h('li', null, 'Item 2')
]);
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Fragments would eliminate unnecessary wrapper elements.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build a Component Abstraction Earlier&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The current API is pure VNodes. No state, no lifecycle beyond hooks.&lt;br&gt;
Adding lightweight components would make Weave actually practical:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function Counter({ initialValue }) {
  const [count, setCount] = useState(initialValue);

  return h('div', null, [
    h('h1', null, String(count)),
    h('button', { onclick: () =&amp;gt; setCount(count + 1) }, '+1')
  ]);
}
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;This is the natural next step.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Profile Much Earlier&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I didn't add metrics until after building most of the core.&lt;br&gt;
Some decisions (like always freezing children arrays) might have been different if I'd measured from the start.&lt;br&gt;
Next time: metrics first, optimizations second.&lt;/p&gt;


&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Short-term (weeks):&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Fragment support - Allow returning arrays without wrappers&lt;/li&gt;
&lt;li&gt;Component abstraction - Lightweight state management&lt;/li&gt;
&lt;li&gt;Performance benchmarks - Compare against React/Preact&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Medium-term (months):&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Async rendering - Interruptible updates, priority-based scheduling&lt;/li&gt;
&lt;li&gt;Additional platforms - Canvas renderer, Terminal UI&lt;/li&gt;
&lt;li&gt;Developer tools - Browser extension, component inspector&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Long-term (ambitious):&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;JSX support - Better DX than raw h() calls&lt;/li&gt;
&lt;li&gt;Server-side rendering - Render to HTML string + client hydration&lt;/li&gt;
&lt;li&gt;Small ecosystem - Router, forms, state management&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;Github : 

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Symphony007" rel="noopener noreferrer"&gt;
        Symphony007
      &lt;/a&gt; / &lt;a href="https://github.com/Symphony007/Weave---VirtualDOM" rel="noopener noreferrer"&gt;
        Weave---VirtualDOM
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A renderer-agnostic Virtual DOM engine built from scratch
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Weave VDOM&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A production-grade Virtual DOM reconciler built from scratch in TypeScript. Weave implements a platform-agnostic diffing algorithm with keyed reconciliation, lifecycle hooks, and strict immutability guarantees.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.typescriptlang.org/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5df0400b2f5598241dae8e55123f6eb21c93fd6d69647e68fc17d4105bcb61b0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f547970655363726970742d352e302b2d626c75652e737667" alt="TypeScript"&gt;&lt;/a&gt;
&lt;a href="https://opensource.org/licenses/MIT" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fdf2982b9f5d7489dcf44570e714e3a15fce6253e0cc6b5aa61a075aac2ff71b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e737667" alt="License: MIT"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Overview&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Weave is a minimal yet complete Virtual DOM implementation that demonstrates the core concepts behind modern UI libraries like React, Vue, and Preact.&lt;/p&gt;
&lt;p&gt;It provides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Platform-agnostic core&lt;/strong&gt; — The diffing algorithm has zero DOM dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Efficient reconciliation&lt;/strong&gt; — Keyed and non-keyed diffing with minimal DOM operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type safety&lt;/strong&gt; — Full TypeScript support with strict type checking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance metrics&lt;/strong&gt; — Built-in instrumentation for debugging and optimization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production-ready patterns&lt;/strong&gt; — Immutable VNodes, stable identity, and declarative patches&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Why Weave?&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;Weave was built to understand Virtual DOM internals from first principles.
Unlike production libraries that prioritize features and bundle size, Weave prioritizes &lt;strong&gt;clarity and correctness&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Every design decision is explicit, making it an excellent learning resource or foundation for custom renderers.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick Start&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Installation&lt;/h3&gt;…&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Symphony007/Weave---VirtualDOM" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




</description>
      <category>javascript</category>
      <category>react</category>
      <category>showdev</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
