Kodokon kodokon.com

El pipeline de renderizado: reflow, repaint, compositing

Comprende las cuatro fases del renderizado del navegador para saber exactamente qué propiedades CSS son costosas y cómo funciona will-change por dentro.

11 min · 3 preguntas

Abrir esta lección en Kodokon

El navegador convierte tu CSS en píxeles mediante un pipeline de cuatro fases: Style (calcular los valores), Layout (geometría, también conocido como reflow), Paint (rasterizar en píxeles, el repaint) y Composite (ensamblar las capas en la GPU). La regla de oro: cuanto más temprana sea la fase que tocas, mayor será la factura, porque cada fase posterior se vuelve a ejecutar. Cambiar width o top desencadena layout + paint + composite. Cambiar color o box-shadow se salta el layout pero repinta. Solo transform y opacity pueden ser gestionados por el hilo del compositor en solitario: la animación se mantiene fluida incluso cuando el hilo principal está bloqueado por JavaScript.

CSS
.slide-bad {
  position: absolute;
  transition: left 0.3s ease;
}
.slide-bad:hover { left: 120px; }

.slide-good {
  transition: transform 0.3s ease;
}
.slide-good:hover { transform: translateX(120px); }
Dos animaciones visualmente idénticas: la primera fuerza un reflow en cada fotograma, la segunda se mantiene en el compositor.

El navegador es perezoso: agrupa tus escrituras de estilo y solo recalcula el layout en el siguiente refresco. Pero algunas lecturas de JavaScript - offsetHeight, getBoundingClientRect(), scrollTop, getComputedStyle() sobre una propiedad geométrica - exigen valores actualizados. Si hay una escritura pendiente, el navegador debe recalcular inmediatamente y de forma síncrona: eso es un reflow forzado. Alternar lecturas y escrituras dentro de un bucle produce layout thrashing: un reflow completo por iteración en lugar de uno solo para todo el lote.

JAVASCRIPT
const items = document.querySelectorAll('.item');

items.forEach((item) => {
  const h = item.offsetHeight;
  item.style.height = h * 2 + 'px';
});

const heights = [...items].map((i) => i.offsetHeight);
items.forEach((item, index) => {
  item.style.height = heights[index] * 2 + 'px';
});
Versión 1: un reflow forzado por elemento. Versión 2: primero todas las lecturas, luego todas las escrituras - un solo reflow.

El compositing divide la página en capas que se rasterizan de forma independiente y las ensambla la GPU. will-change: transform pide que el elemento sea promovido a su propia capa antes de la animación, evitando la costosa rasterización en el primer fotograma. Una sutileza de la especificación que a menudo se pasa por alto: will-change debe producir los mismos efectos secundarios que la propiedad que anuncia. Así que will-change: transform crea un contexto de apilamiento y convierte el elemento en un bloque contenedor para sus descendientes position: fixed - exactamente como un transform real. Una simple pista de rendimiento puede, por tanto, romper tu orden de apilamiento o tus elementos fijos.

JAVASCRIPT
const panel = document.querySelector('.panel');

panel.addEventListener('pointerenter', () => {
  panel.style.willChange = 'transform';
});

panel.addEventListener('transitionend', () => {
  panel.style.willChange = 'auto';
});
El patrón recomendado por la especificación: promover justo antes de la animación, liberar justo después.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Qué animación puede ser gestionada íntegramente por el hilo del compositor, sin reflow ni repaint?
    • height de 0 a 200px
    • margin-left de 0 a 100px
    • transform: translateX(100px)
    • box-shadow al pasar el cursor
  2. ¿Por qué leer offsetHeight dentro de un bucle que además modifica estilos resulta tan costoso?
    • Cada lectura fuerza un reflow síncrono si hay una escritura pendiente
    • offsetHeight desencadena un repaint en la GPU en cada acceso
    • La propiedad nunca es cacheada por el motor de JavaScript
  3. ¿Qué efecto secundario comparte will-change: transform con un transform real?
    • Deshabilita las transiciones CSS del elemento
    • Crea un contexto de apilamiento y un bloque contenedor para position: fixed
    • Fuerza el elemento a display: block
    • Duplica la resolución de rasterización