TechLead
🌳
Intermedio
11 min lectura

Rendimiento de manipulación del DOM

Operaciones DOM eficientes, reflows, repaints y optimización de layout

Comprender el renderizado del navegador

El proceso de renderizado implica construcción del DOM, cálculo de estilos, layout (reflow), paint y compositing. Entender estos pasos es clave para optimizar rendimiento.

Reflows vs Repaints

  • Reflow: Recalcula posiciones y tamaños (caro)
  • Repaint: Redibuja píxeles (más barato que reflow)
  • Composite: Combina capas (lo más barato)

Layout thrashing

El layout thrashing ocurre cuando lees y escribes en el DOM repetidamente, forzando múltiples 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)`;
}

Manipulación eficiente del DOM

// ❌ 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); }

Propiedades CSS que disparan 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; }
}

Medición de rendimiento

// 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';
  });
}

Patrones de Virtual DOM

// 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;
  }
}

Buenas prácticas

  • Separa lecturas y escrituras de DOM en batches
  • Usa transform y opacity para animaciones
  • Minimiza layouts síncronos forzados (offsetWidth, getComputedStyle)
  • Usa DocumentFragment para múltiples inserciones
  • Evita estilos inline: usa clases CSS
  • Usa will-change con moderación en elementos animados
  • Aplica debounce a handlers de resize y scroll
  • Usa content-visibility: auto para contenido fuera de pantalla
  • Implementa virtual scrolling en listas largas
  • Cachea consultas DOM y evita buscarlas repetidamente