As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!
Building a virtual DOM implementation has become one of the most transformative experiences in my journey as a JavaScript developer. The concept seems deceptively simple at first—create lightweight JavaScript objects that mirror the actual DOM structure—but the implementation reveals layers of complexity that demand careful consideration.
Let me walk you through the essential techniques I've found most valuable when building virtual DOM systems.
The foundation starts with creating virtual node objects. These objects need to capture everything about a DOM element while remaining memory-efficient. I typically structure them to include the element type, properties, children, and a key for tracking.
class VNode {
constructor(tag, props, children) {
this.tag = tag;
this.props = props || {};
this.children = children || [];
this.key = props?.key;
this.domNode = null;
}
}
What surprised me most was how much performance improvement comes from proper key assignment. When working with lists, keys help the diffing algorithm understand which items moved rather than assuming everything changed.
Creating a virtual DOM library requires a factory function for generating these nodes. I prefer a function that handles various edge cases while maintaining a clean API.
class VirtualDOM {
createElement(tag, props, ...children) {
// Handle text nodes and boolean values
const flatChildren = children.flat().filter(child =>
child !== null && child !== false && child !== undefined
);
return new VNode(tag, props, flatChildren);
}
}
The real magic happens in the diffing algorithm. This is where we compare the previous virtual DOM tree with the new one to determine what actually changed. I've found that a depth-first approach works best, checking nodes level by level.
diff(oldVNode, newVNode) {
if (!oldVNode) {
return { type: 'CREATE', node: newVNode };
}
if (!newVNode) {
return { type: 'REMOVE' };
}
if (this.isVNodeChanged(oldVNode, newVNode)) {
return { type: 'REPLACE', node: newVNode };
}
if (newVNode.tag) {
const propsDiff = this.diffProps(oldVNode.props, newVNode.props);
const childrenDiff = this.diffChildren(oldVNode.children, newVNode.children);
return {
type: 'UPDATE',
props: propsDiff,
children: childrenDiff
};
}
return { type: 'SKIP' };
}
Property comparison requires special attention. I optimize this by only comparing properties that actually exist in either the old or new props object, avoiding unnecessary iterations.
diffProps(oldProps, newProps) {
const patches = {};
const allProps = new Set([...Object.keys(oldProps), ...Object.keys(newProps)]);
allProps.forEach(prop => {
if (!(prop in newProps)) {
patches[prop] = null;
} else if (!(prop in oldProps) || newProps[prop] !== oldProps[prop]) {
patches[prop] = newProps[prop];
}
});
return patches;
}
The patching process transforms these differences into actual DOM operations. This is where we bridge the gap between our virtual representation and the real browser DOM. I separate node creation from patching to keep responsibilities clear.
createDOMNode(vnode) {
let node;
if (typeof vnode === 'string') {
node = document.createTextNode(vnode);
} else {
node = document.createElement(vnode.tag);
// Handle different property types
Object.keys(vnode.props).forEach(prop => {
if (prop.startsWith('on') && typeof vnode.props[prop] === 'function') {
const eventType = prop.toLowerCase().substring(2);
node.addEventListener(eventType, vnode.props[prop]);
} else if (prop === 'className') {
node.className = vnode.props[prop];
} else if (prop === 'style' && typeof vnode.props[prop] === 'object') {
Object.assign(node.style, vnode.props[prop]);
} else {
node.setAttribute(prop, vnode.props[prop]);
}
});
// Recursively create children
vnode.children.forEach(child => {
const childNode = this.createDOMNode(child);
node.appendChild(childNode);
});
vnode.domNode = node;
}
return node;
}
Event handling presents an interesting challenge. Early in my virtual DOM experiments, I attached event listeners directly to each node. This worked but consumed significant memory in large applications. The solution was event delegation.
class EventDelegator {
constructor(root) {
this.root = root;
this.handlers = new Map();
this.setupDelegation();
}
setupDelegation() {
this.root.addEventListener('click', (e) => {
this.handleEvent('click', e);
});
// Add other event types as needed
}
handleEvent(type, event) {
let target = event.target;
while (target && target !== this.root) {
const handler = this.handlers.get(target);
if (handler && handler[type]) {
handler[type](event);
break;
}
target = target.parentNode;
}
}
}
Memory management became crucial when building long-running applications. I implemented reference cleaning during component unmounting and used object pooling for frequently created nodes.
class NodePool {
constructor() {
this.pool = new Map();
}
get(tag) {
if (!this.pool.has(tag)) {
this.pool.set(tag, []);
}
const pool = this.pool.get(tag);
return pool.length > 0 ? pool.pop() : document.createElement(tag);
}
recycle(node) {
// Clear properties and content
while (node.firstChild) {
node.removeChild(node.firstChild);
}
const tag = node.tagName.toLowerCase();
if (!this.pool.has(tag)) {
this.pool.set(tag, []);
}
this.pool.get(tag).push(node);
}
}
Performance monitoring helped me identify bottlenecks. I added timing measurements to understand where the virtual DOM spent most of its time.
class PerformanceMonitor {
constructor() {
this.metrics = {
diffTime: 0,
patchTime: 0,
renderCount: 0
};
}
startTimer() {
return performance.now();
}
recordDiffTime(startTime) {
this.metrics.diffTime += performance.now() - startTime;
}
recordPatchTime(startTime) {
this.metrics.patchTime += performance.now() - startTime;
}
getMetrics() {
return {
...this.metrics,
averageDiffTime: this.metrics.diffTime / this.metrics.renderCount,
averagePatchTime: this.metrics.patchTime / this.metrics.renderCount
};
}
}
Batch updating proved essential for handling rapid state changes. Without batching, multiple consecutive updates could cause layout thrashing and poor performance.
class BatchUpdater {
constructor() {
this.updates = [];
this.scheduled = false;
}
enqueue(update) {
this.updates.push(update);
this.scheduleUpdate();
}
scheduleUpdate() {
if (!this.scheduled) {
this.scheduled = true;
requestAnimationFrame(() => this.flushUpdates());
}
}
flushUpdates() {
while (this.updates.length) {
const update = this.updates.shift();
update();
}
this.scheduled = false;
}
}
Server-side rendering required additional considerations. The virtual DOM needed to serialize to HTML strings while maintaining compatibility with client-side hydration.
class ServerRenderer {
static renderToString(vnode) {
if (typeof vnode === 'string') {
return vnode;
}
const props = Object.keys(vnode.props)
.map(key => {
if (key === 'className') {
return `class="${vnode.props[key]}"`;
}
if (key === 'style' && typeof vnode.props[key] === 'object') {
const styles = Object.keys(vnode.props[key])
.map(styleKey => `${styleKey}:${vnode.props[key][styleKey]}`)
.join(';');
return `style="${styles}"`;
}
if (!key.startsWith('on')) {
return `${key}="${vnode.props[key]}"`;
}
return '';
})
.filter(Boolean)
.join(' ');
const children = vnode.children
.map(child => this.renderToString(child))
.join('');
return `<${vnode.tag} ${props}>${children}</${vnode.tag}>`;
}
}
Component lifecycle integration brought everything together. I needed to ensure virtual DOM updates triggered at the right moments while providing optimization opportunities.
class Component {
constructor(props) {
this.props = props;
this.state = {};
this.vnode = null;
}
setState(updater) {
const newState = typeof updater === 'function'
? updater(this.state)
: updater;
this.state = { ...this.state, ...newState };
if (this.shouldComponentUpdate(this.props, this.state)) {
this.updateComponent();
}
}
shouldComponentUpdate(nextProps, nextState) {
return true; // Default implementation
}
updateComponent() {
const newVNode = this.render();
const patches = this.vdom.diff(this.vnode, newVNode);
this.vdom.patch(this.domNode.parentNode, patches);
this.vnode = newVNode;
}
}
Throughout my work with virtual DOM implementations, I've learned that the most effective solutions balance simplicity with performance. The techniques I've shared represent years of refinement and practical application. Each optimization, from efficient diffing to proper memory management, contributes to creating responsive applications that scale gracefully.
The virtual DOM approach fundamentally changes how we think about UI development. Instead of manually manipulating the DOM, we describe what we want and let the system figure out the most efficient way to make it happen. This declarative model has proven incredibly powerful in building maintainable, performant web applications.
What started as an experiment has become an essential tool in modern web development. The techniques continue to evolve, but the core principles remain remarkably consistent. Understanding these fundamentals provides a solid foundation for working with any virtual DOM-based framework.
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva
Top comments (0)