Qué propiedad dispara qué etapa, y de dónde sacar el dato
Los tres niveles de coste de una propiedad de CSS, por qué las tablas de referencia que circulan están desactualizadas, y el banco de pruebas que te da la respuesta correcta para tu navegador y tu página.
La pregunta “¿esta propiedad provoca layout?” tiene una respuesta que depende del motor, de la versión, de si el elemento tiene su propia capa y de qué más hay alrededor. Las tablas de referencia que todo el mundo consulta se publicaron hace años y describen motores que ya no existen. La respuesta fiable no está en una tabla: está en un banco de pruebas de veinte líneas que se ejecuta en el navegador que te importa, y hay que saber montarlo.
- Clasificar una propiedad en los tres niveles de coste y justificar la clasificación.
- Explicar por qué
transformno siempre es solo composición. - Montar un banco que mida el coste real de animar una propiedad.
- Leer el desglose de un fotograma con la API de fotogramas largos de animación.
Los tres niveles
Nivel uno: dispara layout. El cambio altera la geometría de alguna caja, así que hay que recalcular posiciones y tamaños, y después pintar y componer. Es el nivel caro.
Entran aquí todas las propiedades que participan en el modelo de caja o en el flujo: width, height, min-block-size y familia, padding, margin, border-width, display, position, float, top, left, inset, flex-basis, flex-grow, grid-template-columns, gap, align-items, justify-content, font-size, font-family, font-weight, line-height, letter-spacing, word-spacing, white-space, text-align, overflow, vertical-align.
Fíjate en las tipográficas de esa lista: cambiar la familia o el peso de la fuente es un cambio de layout, porque cambian las anchuras de los glifos. Es la razón técnica de todo lo que vimos sobre el desplazamiento que produce el intercambio de fuente.
Nivel dos: dispara pintura. El cambio altera el aspecto pero no la geometría. Se salta el layout y se repinta.
color, background-color, background-image, background-position, border-color, border-radius, box-shadow, text-shadow, outline, visibility, text-decoration, list-style.
Nivel tres: solo composición. El compositor lo resuelve con la textura que ya tiene, sin repintar nada.
transform y sus longitudinales individuales translate, rotate y scale; opacity; filter; backdrop-filter.
Y ahora el matiz que hace que la lista de arriba sea insuficiente por sí sola.
Por qué transform no siempre es solo composición
Un cambio solo se puede resolver en el compositor si el elemento tiene su propia capa de composición. Si no la tiene, el elemento forma parte de la textura de otra capa, y mover el elemento obliga a repintar esa textura.
Los motores promocionan automáticamente a los elementos con una animación de transform o de opacity gestionada por el motor, es decir, una transición o animación de CSS, o una animación creada con la API de animaciones web. Ese es el caso en que la promesa se cumple sin más.
Lo que no se promociona automáticamente es un cambio puntual escrito desde JavaScript:
// Esto NO es necesariamente solo composicion
caja.style.transform = `translateX(${x}px)`;
Si caja no tiene capa propia, cada asignación repinta la capa a la que pertenece. Con un elemento pequeño sobre un fondo simple no se nota; con un elemento grande sobre un fondo complejo, es tan caro como animar left.
Las dos formas correctas de conseguir el nivel tres de verdad:
/* Opcion A: que la animacion la gestione el motor */
.panel {
transition: transform 240ms ease-out;
}
.panel[data-abierto] {
transform: translateX(0);
}
// Opcion B: la API de animaciones, que tambien se promociona
caja.animate(
[{ transform: 'translateX(0)' }, { transform: 'translateX(320px)' }],
{ duration: 240, easing: 'ease-out', fill: 'forwards' },
);
Cuando de verdad necesitas mover algo por código en cada fotograma —un arrastre, un elemento que sigue al puntero— la promoción hay que pedirla explícitamente, y eso tiene su propio coste que trata la lección siguiente.
Un segundo matiz sobre filter: es compuesto en el sentido de que el compositor sabe aplicarlo, y el coste por fotograma lo paga la GPU, que no es gratis. Un desenfoque grande sobre un área grande puede costar más milisegundos de GPU que lo que habría costado repintar. Que una propiedad esté en el nivel tres significa que no consume hilo principal, no que sea gratis.
De dónde sacar el dato
La referencia que todo el mundo enlaza cuando surge esta pregunta lleva años sin actualizarse y sus datos corresponden a versiones de los motores muy anteriores a las actuales. Usarla no es catastrófico para las propiedades obvias y sí lo es para los casos límite, que son precisamente los que te llevan a consultarla.
Las tres fuentes que sí son fiables, en orden de utilidad práctica.
Uno: el banco de pruebas. Mides el coste de animar la propiedad en tu página, en tu navegador. Es el único dato que no puede estar desactualizado por definición, y es el que sigue abajo.
Dos: la superposición de destellos de pintura. En el panel de renderizado de las herramientas de desarrollo, la casilla de destellos de pintura tiñe de verde cualquier zona que se repinte. Cambias la propiedad y miras: si no hay destello, no hubo pintura. Es una respuesta binaria e inmediata a la pregunta “¿esto repinta?”, que es la mitad del problema.
Tres: el propio desglose del panel de rendimiento. Grabas mientras el cambio ocurre en bucle y miras qué eventos aparecen. Si aparece un bloque de layout, hubo layout. No hay ambigüedad posible.
Y una fuente auxiliar que merece conocerse: la auditoría automática de animaciones no compuestas. Las herramientas de auditoría incluyen una comprobación que lista las animaciones de la página que no se pudieron ejecutar en el compositor, con el motivo concreto de cada una: que la propiedad no es compatible, que el efecto tiene un objetivo no compatible, que hay un filtro que lo impide. Ese motivo es la información más difícil de obtener por otros medios y llega gratis.
El banco
Dos mediciones complementarias. La primera mide el intervalo entre fotogramas, que revela si se están perdiendo:
function bancoDeFotogramas(aplicar, fotogramas = 240) {
return new Promise((resolver) => {
const intervalos = [];
let previo = performance.now();
let i = 0;
requestAnimationFrame(function paso(t) {
intervalos.push(t - previo);
previo = t;
aplicar(i);
if (++i < fotogramas) requestAnimationFrame(paso);
else {
const orden = intervalos.slice(2).sort((a, b) => a - b);
resolver({
mediana: +orden[Math.floor(orden.length * 0.5)].toFixed(2),
p95: +orden[Math.floor(orden.length * 0.95)].toFixed(2),
perdidos: intervalos.filter((d) => d > 20).length,
});
}
});
});
}
const caja = document.querySelector('#sujeto');
console.log('left ', await bancoDeFotogramas((i) => { caja.style.left = `${i % 240}px`; }));
console.log('transform', await bancoDeFotogramas((i) => { caja.style.transform = `translateX(${i % 240}px)`; }));
console.log('bgcolor ', await bancoDeFotogramas((i) => { caja.style.backgroundColor = i % 2 ? '#89b4fa' : '#a6e3a1'; }));
console.log('boxshadow', await bancoDeFotogramas((i) => { caja.style.boxShadow = `0 0 ${i % 60}px #1e1e2e`; }));
Una advertencia sobre cómo interpretarlo: en una página vacía las cuatro filas van a dar lo mismo, alrededor de 16,7 milisegundos, porque el ritmo lo marca la pantalla y sobra tiempo para todo. El banco solo distingue si la página tiene suficiente contenido para que las diferencias importen. Ejecútalo en tu página real, no en una de laboratorio, y si quieres exagerar las diferencias, estrangula la CPU en las herramientas de desarrollo.
La segunda medición es la buena, porque da el desglose y no solo el total. La API de fotogramas largos de animación entrega, para cada fotograma que se pasa del presupuesto, cuándo empezó, cuándo empezó el renderizado y cuándo empezaron el estilo y el layout:
new PerformanceObserver((lista) => {
for (const f of lista.getEntries()) {
console.table([{
inicio: Math.round(f.startTime),
total: Math.round(f.duration),
script: Math.round(f.renderStart - f.startTime),
renderizado: Math.round(f.startTime + f.duration - f.renderStart),
estiloYLayout: Math.round(f.startTime + f.duration - f.styleAndLayoutStart),
bloqueante: Math.round(f.blockingDuration),
}]);
}
}).observe({ type: 'long-animation-frame', buffered: true });
Las columnas dicen exactamente lo que hace falta saber. script es lo que tardó tu código antes de que empezara el renderizado. renderizado es todo lo que costó producir el fotograma a partir de ahí. estiloYLayout es la parte de ese renderizado que se fue en recalcular estilo y geometría. Si script es pequeño y estiloYLayout es grande, tu problema no es el código: es lo que le estás pidiendo al motor que recalcule.
Esta API es de disponibilidad limitada a los navegadores basados en Chromium en el momento de escribir esto, así que sirve para diagnosticar y no para instrumentar todo tu tráfico. Para diagnosticar es exactamente lo que hacía falta.
Hay un error de razonamiento muy extendido que sobrevive a todas las tablas de propiedades, y consiste en creer que el coste de un cambio es una propiedad del cambio. No lo es. El coste es el producto de la etapa por el número de elementos que la etapa tiene que recorrer, y el segundo factor domina con muchísima diferencia.
Un ejemplo que lo hace obvio. Cambiar background-color está en el nivel dos, el barato. Cambiar width está en el nivel uno, el caro. Y sin embargo:
- Cambiar
widthen un elemento hoja, absolutamente posicionado, sin hermanos afectados: menos de un milisegundo. El layout tiene que recalcular una caja. - Cambiar
background-coloren una regla que empareja con cuatro mil elementos: entre 15 y 60 milisegundos. No hay layout, y hay cuatro mil recálculos de estilo y una lista de dibujo enorme que regenerar.
La propiedad “cara” sale cien veces más barata que la “barata”. La tabla no mentía; la tabla solo describía la mitad de la ecuación.
De ahí salen tres consecuencias que cambian cómo se optimiza.
La primera: antes de cambiar de propiedad, reduce el alcance. Si un cambio cuesta caro, la primera pregunta no es “¿qué propiedad puedo usar en su lugar?” sino “¿a cuántos elementos está afectando esto y por qué a tantos?”. Añadir una clase a un contenedor raíz para cambiar un tema invalida el documento entero; hacerlo con una propiedad personalizada heredada también, porque la herencia se propaga. Aplicar el cambio al subárbol concreto que lo necesita puede dividir el coste por cien.
La segunda: la ventana visible acota el trabajo de pintura y rasterizado, pero no el de estilo ni el de layout. El motor no rasteriza lo que no se ve, así que la pintura de una lista de diez mil filas fuera de pantalla no cuesta. El recálculo de estilo y el layout de esas diez mil filas sí cuestan, porque para saber dónde está cada cosa hay que calcular dónde está cada cosa. Es la razón profunda de que el número de nodos del documento sea el factor que más veces aparece detrás de un problema de renderizado.
La tercera, y es la que más tiempo ahorra en la práctica: mide el número de elementos afectados, no solo el tiempo. El panel de rendimiento, al seleccionar un evento de recálculo de estilo, indica cuántos elementos se afectaron; al seleccionar uno de layout, cuántos nodos necesitaban layout. Esas dos cifras son mucho más accionables que los milisegundos, porque los milisegundos dependen del dispositivo y el recuento no. Un recálculo que afecta a cuatro mil elementos es un problema en cualquier dispositivo, aunque en tu portátil cueste ocho milisegundos.
Y el corolario para el diseño de componentes: un componente que al cambiar de estado invalida a sus ancestros es un componente mal aislado. Lo correcto es que la unidad de cambio sea lo más profunda posible en el árbol. Escribir el estado como un atributo en el elemento raíz de la aplicación y estilar con selectores descendentes desde ahí es cómodo de escribir y garantiza que cada cambio de estado recorra el documento entero.
Elige los tres cambios de estilo más frecuentes de tu interfaz —los que ocurren en cada interacción— y pásalos por el banco de fotogramas en la página real, con la CPU estrangulada a cuatro veces. Después graba un perfil de cada uno y anota, además de los milisegundos, el número de elementos afectados en el recálculo de estilo. Si alguno pasa de mil, ahí tienes tu trabajo, y no es cambiar de propiedad.