🌳
Intermediate
11 min read

DOM Manipulation Performance

Efficient DOM operations, reflows, repaints, and layout optimization

Understanding Browser Rendering

The browser rendering process involves several steps: DOM construction, style calculation, layout (reflow), paint, and compositing. Understanding these steps is crucial for performance optimization.

Reflows vs Repaints

  • Reflow: Recalculates element positions and dimensions (expensive)
  • Repaint: Redraws pixels (less expensive than reflow)
  • Composite: Combines layers (cheapest)

Layout Thrashing

Layout thrashing occurs when you repeatedly read and write to the DOM, forcing multiple reflows:

// āŒ Bad: Layout thrashing - reading and writing in a loop
function resizeElements() {
  elements.forEach(element => {
    const width = element.offsetWidth; // Read (forces reflow)
    element.style.width = width + 10 + 'px'; // Write (invalidates layout)
  });
}

// āœ… Good: Batch reads, then batch writes
function resizeElementsOptimized() {
  // First, read all values
  const widths = elements.map(el => el.offsetWidth);
  
  // Then, write all values
  elements.forEach((element, i) => {
    element.style.width = widths[i] + 10 + 'px';
  });
}

// āœ… Best: Use CSS transforms instead of layout properties
function moveElement(element, x, y) {
  // āŒ Bad: Triggers reflow
  element.style.left = x + 'px';
  element.style.top = y + 'px';
  
  // āœ… Good: Uses compositing only
  element.style.transform = `translate(${x}px, ${y}px)`;
}

Efficient DOM Manipulation

// āŒ Bad: Multiple DOM insertions
function addItems(items) {
  const container = document.getElementById('container');
  items.forEach(item => {
    const div = document.createElement('div');
    div.textContent = item;
    container.appendChild(div); // Multiple reflows
  });
}

// āœ… Good: Use DocumentFragment
function addItemsOptimized(items) {
  const fragment = document.createDocumentFragment();
  items.forEach(item => {
    const div = document.createElement('div');
    div.textContent = item;
    fragment.appendChild(div);
  });
  container.appendChild(fragment); // Single reflow
}

// āœ… Better: Use innerHTML for large updates
function addItemsWithHTML(items) {
  const html = items.map(item => `
${item}
`).join(''); container.innerHTML = html; // Single reflow } // āœ… Best: Use insertAdjacentHTML function addItemsEfficient(items) { const html = items.map(item => `
${item}
`).join(''); container.insertAdjacentHTML('beforeend', html); }

CSS Properties that Trigger Reflows

// Properties that trigger reflow (expensive):
// width, height, margin, padding, border, position, top, left, bottom, right
// display, float, clear, overflow, font-size, line-height

// Properties that only trigger repaint (cheaper):
// color, background-color, visibility, box-shadow, outline

// Properties that only trigger composite (cheapest):
// transform, opacity

// āœ… Example: Animating with transform
@keyframes slide {
  from { transform: translateX(0); }
  to { transform: translateX(100px); }
}

// āŒ Avoid animating layout properties
@keyframes slideWrong {
  from { left: 0; }
  to { left: 100px; }
}

Measuring Performance

// Detect layout thrashing
function measureReflows() {
  let reflowCount = 0;
  
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (entry.name === 'Layout') {
        reflowCount++;
      }
    }
  });
  
  observer.observe({ entryTypes: ['measure'] });
  
  // Your DOM operations here
  
  console.log('Reflows:', reflowCount);
}

// Use requestAnimationFrame for smooth updates
function updateUI() {
  requestAnimationFrame(() => {
    // All DOM reads
    const height = element.offsetHeight;
    const width = element.offsetWidth;
    
    // All DOM writes
    element.style.height = height * 2 + 'px';
    element.style.width = width * 2 + 'px';
  });
}

Virtual DOM Patterns

// Implement simple virtual DOM diffing
class VirtualDOM {
  constructor() {
    this.current = null;
  }
  
  render(vnode, container) {
    if (!this.current) {
      // Initial render
      container.appendChild(this.createElement(vnode));
      this.current = vnode;
    } else {
      // Update: diff and patch
      this.updateElement(container, vnode, this.current);
      this.current = vnode;
    }
  }
  
  createElement(vnode) {
    if (typeof vnode === 'string') {
      return document.createTextNode(vnode);
    }
    
    const el = document.createElement(vnode.tag);
    
    // Set attributes
    Object.keys(vnode.attrs || {}).forEach(key => {
      el.setAttribute(key, vnode.attrs[key]);
    });
    
    // Append children
    (vnode.children || []).forEach(child => {
      el.appendChild(this.createElement(child));
    });
    
    return el;
  }
  
  updateElement(parent, newNode, oldNode, index = 0) {
    // Implement minimal DOM updates
    if (!oldNode) {
      parent.appendChild(this.createElement(newNode));
    } else if (!newNode) {
      parent.removeChild(parent.childNodes[index]);
    } else if (this.changed(newNode, oldNode)) {
      parent.replaceChild(
        this.createElement(newNode),
        parent.childNodes[index]
      );
    }
  }
  
  changed(node1, node2) {
    return typeof node1 !== typeof node2 ||
           (typeof node1 === 'string' && node1 !== node2) ||
           node1.tag !== node2.tag;
  }
}

Best Practices

  • Batch DOM reads and writes separately
  • Use transform and opacity for animations
  • Minimize forced synchronous layouts (reading offsetWidth, getComputedStyle)
  • Use DocumentFragment for multiple insertions
  • Avoid inline styles - use CSS classes instead
  • Use will-change CSS property sparingly for animated elements
  • Debounce resize and scroll event handlers
  • Use content-visibility: auto for off-screen content
  • Implement virtual scrolling for long lists
  • Cache DOM queries - don't repeatedly query for the same element