Optimización del rendimiento en el desarrollo web
La optimización del rendimiento es crucial en el desarrollo web para garantizar aplicaciones rápidas, receptivas y eficientes. Esta guía cubre técnicas para optimizar el DOM
El DOM es la operación más costosa que la mayoría de las aplicaciones JavaScript realizan. Leer una propiedad como offsetHeight puede obligar al navegador a detenerse y recalcular la geometría de la página, y escribir en el DOM puede desencadenar un reflow (recálculo de posiciones y tamaños de los elementos) y un repaint (redibujado de píxeles). Hacer esto descuidadamente dentro de un bucle puede hacer que una página que debería ser instantánea empiece a mostrar problemas de rendimiento.
Esta guía explica por qué el trabajo con el DOM es lento y muestra técnicas concretas para solucionarlo: minimizar el acceso, evitar el layout thrashing, agrupar cambios con requestAnimationFrame y document.createDocumentFragment(), y hacer perfiles de lo que no se puede adivinar.
Por qué las operaciones DOM son lentas
JavaScript se ejecuta en un motor rápido, pero el DOM es el límite entre ese motor y el pipeline de renderizado del navegador. Cruzar ese límite repetidamente es lo que genera problemas:
- Reflow (layout) — el navegador recalcula la posición y el tamaño de los elementos. Un reflow en un elemento puede propagarse a sus ancestros, descendientes y hermanos.
- Repaint — el navegador redibuja los píxeles (colores, sombras, visibilidad) sin cambiar la geometría. Es más barato que el reflow, pero tampoco es gratuito.
La idea clave: el navegador intenta agrupar estas operaciones por ti. Pone en cola tus escrituras y las descarga de una sola vez, justo antes de que se pinte el siguiente fotograma. Rompes esa optimización en el momento en que lees una propiedad de layout, porque el navegador debe descargar todas las escrituras pendientes de inmediato para darte una respuesta precisa. Esa descarga forzada se llama layout sincrónico (forzado), y hacerlo dentro de un bucle es la raíz de la mayoría de los problemas de rendimiento del DOM.
Minimizar el acceso al DOM
Cada lectura y escritura de una propiedad cruza el límite entre JS y el DOM, por lo que la optimización más barata es hacer menos de eso.
- Guardar en caché las referencias a elementos. Busca un elemento una vez y guárdalo en una variable en lugar de llamar a
document.querySelectoren cada iteración. - Leer en variables locales. Los contadores de bucles, las longitudes y los valores calculados pertenecen a variables JavaScript, no deben releerse del DOM en cada pasada.
- Construir cadenas en lugar de nodos cuando sea apropiado. Asignar
innerHTMLuna sola vez suele ser más rápido que insertar muchos nodos uno a uno, aunque se pierden los listeners de eventos y es inseguro con entradas no confiables.
// Slow: re-queries and re-reads the DOM on every iteration
for (let i = 0; i < items.length; i++) {
document.getElementById('list').appendChild(makeRow(items[i]));
}
// Fast: resolve the reference once, outside the loop
const list = document.getElementById('list');
for (let i = 0; i < items.length; i++) {
list.appendChild(makeRow(items[i]));
}Consulta Seleccionar elementos del DOM para elegir selectores rápidos y específicos — prefiere getElementById y querySelector concretos en lugar de cadenas de descendientes profundas como div > ul li span.
Manejo eficiente de eventos
Adjuntar un listener a cada elemento de una lista larga desperdicia memoria y ralentiza las actualizaciones del DOM. La delegación de eventos adjunta un solo listener a un padre compartido y usa event.target para encontrar qué elemento hijo fue activado, aprovechando el burbujeo de eventos.
// One listener handles the whole list, including rows added later
document.getElementById('list').addEventListener('click', (event) => {
const row = event.target.closest('li');
if (row) console.log('clicked row:', row.dataset.id);
});Esto escala a miles de elementos y cubre automáticamente los elementos añadidos después de configurar el listener. Aprende más en Manejo de eventos en el DOM e Introducción a los eventos del navegador.
Comprender y evitar el layout thrashing
¿Qué es el layout thrashing?
El layout thrashing ocurre cuando intercalas lecturas y escrituras de propiedades de layout en rápida sucesión. Cada lectura fuerza un layout sincrónico para descargar la escritura anterior, por lo que un bucle de lectura-escritura-lectura-escritura desencadena un reflow por iteración en lugar de uno solo en total.
// Bad: read (offsetWidth) forces layout, then write invalidates it — every loop
const boxes = document.querySelectorAll('.box');
boxes.forEach((box) => {
box.style.width = box.offsetWidth + 10 + 'px'; // read + write interleaved
});Cómo solucionarlo: agrupar lecturas, luego escrituras
Agrupa primero todas las lecturas y luego realiza todas las escrituras. El navegador realiza una pasada de layout para las lecturas y otra para las escrituras.
const boxes = document.querySelectorAll('.box');
// 1. Read phase — collect every measurement first
const widths = [...boxes].map((box) => box.offsetWidth);
// 2. Write phase — now apply all changes; no read interrupts them
boxes.forEach((box, i) => {
box.style.width = widths[i] + 10 + 'px';
});Programar trabajo con requestAnimationFrame
requestAnimationFrame ejecuta tu callback justo antes del siguiente repaint, que es el momento ideal para escribir cambios en el DOM — se fusionan en un solo fotograma en lugar de desencadenar pinturas intermedias.
const element = document.getElementById('box');
requestAnimationFrame(() => {
const width = element.offsetWidth; // read
element.style.width = width + 10 + 'px'; // write, applied in the same frame
});Para animaciones, mantén todas las escrituras del DOM dentro del callback de requestAnimationFrame y nunca leas el layout en medio de ellas.
Agrupar cambios en el DOM
Usar document.createDocumentFragment()
document.createDocumentFragment() es un contenedor ligero fuera de pantalla. Los nodos añadidos a un fragmento no forman parte del documento activo, por lo que construirlo no desencadena reflows. Cuando añades el fragmento terminado a la página, el navegador inserta todos sus hijos en una sola operación — un reflow en lugar de uno por nodo.
Ejemplo
<!DOCTYPE html>
<html>
<head>
<title>Batching DOM Changes</title>
</head>
<body>
<div id="container"></div>
<script>
const container = document.getElementById('container');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 40; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
fragment.appendChild(div);
}
container.appendChild(fragment); // Batch update
</script>
</body>
</html>El bucle construye 40 elementos contra el fragmento sin ningún impacto en la página visible. Solo el container.appendChild(fragment) final toca el DOM activo, por lo que el navegador realiza una sola pasada de layout en lugar de 40. (Los motores modernos también permiten añadir un array de nodos en una sola llamada con container.append(...nodes), lo cual también se agrupa.)
Evitar el layout sincrónico forzado
Una versión sutil del thrashing es leer una propiedad de layout justo después de un cambio de estilo, lo que obliga al navegador a recalcular el layout de inmediato:
const box = document.getElementById('box');
box.classList.add('expanded'); // write — queues a layout change
const height = box.offsetHeight; // read — forces layout NOW to answerSi no necesitas el valor de inmediato, aplaza la lectura al siguiente fotograma con requestAnimationFrame, o reestructura el código para que todas las lecturas ocurran antes de cualquier escritura. Las propiedades comunes que desencadenan un layout forzado al leerse incluyen offsetTop/offsetWidth/offsetHeight, clientWidth/clientHeight, scrollTop y getComputedStyle().
Buenas prácticas
- Aplaza el JavaScript no crítico. Añade el atributo
defera las etiquetas<script>para que el navegador siga analizando el HTML y ejecute el script después de que el DOM esté listo. Usaasyncpara scripts de terceros independientes. - Prefiere los toggles de clases en lugar de estilos en línea. Cambiar una clase CSS permite al navegador aplicar muchas reglas de estilo en un solo reflow, en lugar de un reflow por cada asignación de
styleen línea. - Anima
transformyopacity. Estas son compuestas por la GPU y omiten completamente el layout y el paint, a diferencia de animarwidth,topomargin. - Desconectar, mutar, reconectar. Para ediciones pesadas, elimina un subárbol del documento (u ocúltalo con
display: none), cámbialo fuera de pantalla y luego vuélvelo a insertar. - Haz perfiles antes de optimizar. Usa el panel Performance de las DevTools del navegador para encontrar el verdadero cuello de botella en lugar de adivinarlo — consulta Depuración del DOM y herramientas.
La regla de oro: agrupa tus lecturas del DOM, luego agrupa tus escrituras. Cada vez que una lectura sigue a una escritura, el navegador se ve obligado a recalcular el layout de forma sincrónica. Agruparlas le permite hacer el trabajo una vez por fotograma.
Errores comunes
- Llamar a
document.querySelectordentro de un bucle en lugar de guardar en caché el resultado. - Leer
offsetWidth/offsetHeighty escribir estilos en la misma iteración del bucle. - Adjuntar un listener de eventos separado a cada elemento de una lista grande y dinámica en lugar de delegar.
- Animar propiedades que desencadenan layout (
width,left,margin) en lugar detransform/opacity.
Conclusión
El rendimiento del DOM se reduce a una idea: el navegador es rápido para agrupar su trabajo de renderizado, y tu tarea es evitar romper esa agrupación. Guarda en caché las referencias, delega eventos, agrupa las lecturas antes de las escrituras, construye actualizaciones grandes dentro de un DocumentFragment y programa los cambios visuales con requestAnimationFrame. Cuando tengas dudas, haz perfiles — el panel Performance de las DevTools te mostrará exactamente dónde ocurren los reflows. A continuación, profundiza en tu conjunto de herramientas con Manipulación del DOM y Técnicas avanzadas del DOM.