wandres.dev
EL PIPELINE DEL NAVEGADOR · Style, layout, paint, composite

Qué propiedad dispara qué etapa

La tabla completa de propiedades y etapas, el cálculo que explica el factor de cien entre width y transform, y cómo verificarlo en tu propia máquina.

⏱ 19 min

La afirmación “anima transform y opacity, no animes width” es correcta y prácticamente inútil, porque sin el cálculo que la sostiene no se puede extender a las cincuenta propiedades que no aparecen en esa frase. Esta lección cierra el nivel poniendo el número: qué etapas dispara cada familia de propiedades, cuánto trabajo implica cada etapa en unidades comparables, y cómo medirlo tú en lugar de creértelo.

🎯 Al terminar esta lección sabrás
  • Clasificar cualquier propiedad CSS en su nivel de coste sin consultar una tabla.
  • Reconstruir el cálculo que explica la diferencia entre animar width y animar transform.
  • Identificar las propiedades cuyo coste está mal estimado por la sabiduría popular.
  • Medir el coste real de una animación concreta en tu dispositivo.

La regla que sustituye a la tabla

No hace falta memorizar qué propiedad dispara qué. Basta con hacerse dos preguntas sobre el valor que cambia, en este orden.

¿El nuevo valor afecta al espacio que la caja ocupa o a la posición de otra caja? Si la respuesta es sí, dispara layout. Esto incluye todo lo obvio —width, height, padding, margin, border-width, top, left, font-size, line-height— y algunas cosas menos obvias: display, position, float, flex-basis, grid-template-*, white-space, overflow, vertical-align y cualquier propiedad lógica equivalente.

¿El nuevo valor cambia los píxeles de la caja o de su entorno? Si layout dijo que no pero esta dice que sí, dispara pintado. Aquí caen color, background-*, border-color, border-radius, box-shadow, outline, text-decoration, visibility y la mayoría de lo demás.

Si las dos respuestas son no, el cambio se resuelve en composición. Y la lista de propiedades que llegan hasta aquí es corta, porque para no tocar ni geometría ni píxeles propios el cambio tiene que ser una operación que la GPU pueda aplicar sobre una textura ya existente: transform y sus equivalentes independientes translate, rotate y scale, opacity, filter y backdrop-filter.

Las dos últimas merecen un asterisco grande. filter y backdrop-filter no tocan layout y en muchos casos se resuelven en la GPU, pero su coste por fotograma no es el de una matriz: es el de un cálculo por píxel sobre el área afectada. Están en la tercera columna por dónde ocurren, no por lo que cuestan.

Familia Etapas Ejemplos
Geometría Estilo, Layout, Paint, Raster, Draw width, padding, top, font-size, flex-basis
Superficie Estilo, Paint, Raster, Draw color, background, border-radius, box-shadow
Composición Estilo, Draw transform, translate, scale, rotate, opacity
Composición cara Estilo, Draw con cálculo por píxel filter, backdrop-filter

El cálculo del factor de cien

Coge un caso concreto y sigue las dos rutas hasta el final. Un panel de 900 por 600 píxeles CSS en una pantalla con devicePixelRatio de 2, animado durante 300 milisegundos a 60 fotogramas por segundo, es decir 18 fotogramas.

Ruta A: animar width de 900 a 1100.

En cada fotograma, el motor tiene que ejecutar estilo para el panel y su subárbol, ejecutar layout porque el ancho cambió y con él la posición de todo lo que hay dentro y de lo que viene después en el flujo, reconstruir la lista de dibujo de la región afectada, y rasterizarla. La rasterización es el término dominante: la región es de al menos 1100 por 600 píxeles CSS, que a devicePixelRatio 2 son 2200 por 1200, es decir 2 640 000 píxeles por fotograma. En 18 fotogramas, algo más de 47 millones de píxeles escritos y subidos a memoria de textura.

A eso hay que sumarle el layout, que en un subárbol con cien elementos y algún contenedor flex puede rondar el cuarto de milisegundo, y el pintado, que construye unas cuantas centenas de operaciones. Ninguno de los dos es el problema; el problema es que la geometría cambió, y por eso hubo que producir píxeles nuevos.

Ruta B: animar scale de 1 a 1.222.

El panel se rasteriza una vez, al principio, con su tamaño natural: 1800 por 1200 píxeles de dispositivo, una sola vez. A partir de ahí, cada fotograma consiste en que el compositor actualiza la matriz de transformación de esa capa —dieciséis números en coma flotante— y le pide a la GPU que dibuje el rectángulo texturizado en su nueva posición. La GPU hace eso a una velocidad que no está en el mismo orden de magnitud: dibujar un cuadrilátero texturizado es la operación para la que está construida.

En 18 fotogramas: 2 160 000 píxeles rasterizados en total (una vez), más 18 actualizaciones de matriz.

La comparación es 47 millones frente a 2.1 millones de píxeles rasterizados, y eso ignorando que en la ruta B ni siquiera hubo estilo ni layout, y que el trabajo ocurrió fuera del hilo principal. El factor de veintitantos en píxeles se convierte en un factor mucho mayor en tiempo del hilo principal, que es lo que de verdad se cuenta, porque en la ruta B ese tiempo es cero.

Y hay una diferencia cualitativa que el número no captura: en la ruta A, si el hilo principal está ocupado con otra cosa, la animación se para. En la ruta B, no.

Las tres propiedades cuyo coste todo el mundo estima mal

La tabla de arriba se cita en todas partes y aun así hay tres casos donde la intuición popular falla en direcciones opuestas, y los tres aparecen en código real constantemente. El primero es opacity, que se cree gratis y no siempre lo es. Cambiar la opacidad de un elemento que tiene su propia capa es una operación de composición pura. Pero cambiar la opacidad de un elemento con muchos hijos que no tiene capa propia obliga al motor a pintar el subárbol en una superficie intermedia para poder aplicar la opacidad al conjunto —si la aplicara a cada hijo por separado, los solapes se verían mal—, y esa superficie intermedia hay que asignarla, rasterizarla y componerla. La opacidad es barata cuando hay capa y cara cuando hay que crear una superficie intermedia por fotograma. El segundo es box-shadow, que se cree barato porque no toca layout y suele ser lo más caro de una interfaz. Un radio de desenfoque de 40 píxeles sobre una tarjeta implica una convolución sobre un área bastante mayor que la tarjeta, en cada fotograma. Comparado con eso, animar width en un elemento pequeño es un juguete. El tercero es filter: blur() sobre elementos pequeños, que se cree caro y a veces no lo es, porque el coste escala con el área y un desenfoque sobre un icono de 24 píxeles son unos pocos miles de operaciones. La moraleja de los tres es la misma y es la que hay que llevarse del nivel entero: el nombre de la propiedad no determina el coste; lo determinan las etapas que dispara multiplicadas por el área sobre la que actúa. Cualquier regla que solo mire la primera mitad te va a fallar la mitad de las veces.

Medirlo en tu dispositivo

Las tablas y los cálculos sirven para tener criterio; para tomar decisiones hace falta el número de tu máquina, que puede diferir en un orden de magnitud del de la máquina donde escribiste la tabla.

La forma correcta de medir una animación no es cronometrar JavaScript, porque el trabajo caro ocurre después de que tu código termine. Hay que mirar el tiempo de fotograma, y para eso lo más limpio es contar fotogramas perdidos:

// Cuenta fotogramas y mide su distribucion durante una animacion.
function perfilarFrames(duracionMs = 3000) {
  const tiempos = [];
  let anterior = null;
  const fin = performance.now() + duracionMs;

  function paso(t) {
    if (anterior !== null) tiempos.push(t - anterior);
    anterior = t;
    if (t < fin) return requestAnimationFrame(paso);

    tiempos.sort((a, b) => a - b);
    const p = (q) => tiempos[Math.floor(tiempos.length * q)].toFixed(1);
    console.log({
      frames: tiempos.length,
      mediana: p(0.5),
      p95: p(0.95),
      peor: tiempos.at(-1).toFixed(1),
    });
  }
  requestAnimationFrame(paso);
}

Fíjate en que lo que se imprime no es la media. La mediana dice cómo va normalmente; el percentil 95 y el peor dicen qué va a percibir el usuario, porque el sistema visual detecta discontinuidades, no promedios. Una mediana de 6 milisegundos con un peor de 40 es una animación que se ve mal, y una media te lo habría ocultado.

Para atribuir el coste a una etapa concreta, el panel de rendimiento con la opción de ralentización de CPU activada es la herramienta correcta. Ralentizar por cuatro o por seis convierte diferencias invisibles en tu portátil en diferencias obvias, y aproxima el comportamiento de un dispositivo de gama media, que es donde está la mayoría de tus usuarios.

Y una comprobación cualitativa que vale más que muchas medidas: bloquea el hilo principal a propósito y mira si la animación sigue. Es la prueba definitiva de si tu animación llegó a la etapa de composición, y es el tema del nivel siguiente.

// Bloquea el hilo principal medio segundo.
setTimeout(() => {
  const fin = performance.now() + 500;
  while (performance.now() < fin) { /* ocupado */ }
}, 1000);
⚔️ Reconstruye el cálculo con tus números
  1. Elige un contenedor real de tu proyecto y anota sus dimensiones y tu devicePixelRatio.
  2. Calcula los píxeles rasterizados totales de animar su anchura durante 300 milisegundos, y los de animar su escala con el mismo efecto visual.
  3. Implementa las dos versiones y perfílalas con la función de esta lección. Compara la mediana y el peor fotograma.
  4. Repite con la ralentización de CPU a seis veces y anota cuál de las dos se rompe primero.