wandres.dev
EL HILO DEL COMPOSITOR · Qué se anima fuera del hilo principal

Las condiciones exactas para que una animación se promueva

La lista de requisitos que el motor comprueba antes de delegar una animación al compositor, y por qué la lista de propiedades ha crecido desde 2021.

⏱ 18 min

Que un elemento tenga capa es necesario pero no suficiente. Antes de delegar una animación al compositor, el motor comprueba una lista de condiciones sobre el efecto entero, y basta que falle una para que toda la animación vuelva al hilo principal, no solo la parte problemática. Conocer esa lista convierte un fenómeno que parece azaroso —“a veces va suave y a veces no”— en una comprobación mecánica que puedes hacer antes de escribir el código.

🎯 Al terminar esta lección sabrás
  • Enumerar las condiciones que el motor evalúa para promover una animación.
  • Distinguir el conjunto de propiedades garantizado en los tres motores del que solo tiene Chromium.
  • Explicar por qué los porcentajes en transform fueron un problema y cómo se resolvió.
  • Escribir una animación que cumpla todas las condiciones por construcción.

La condición sobre las propiedades

El compositor dibuja una textura con unos parámetros. Solo puede animar por su cuenta lo que sea uno de esos parámetros. Ese es el criterio, y de él sale el conjunto que todos los motores delegan:

  • La familia de las transformaciones: transform y las propiedades independientes translate, rotate y scale.
  • opacity.
  • filter y backdrop-filter, con la advertencia de que su coste por fotograma no es el de una matriz sino el de un cálculo por píxel.

Ese conjunto no ha cambiado en años y es el que puedes dar por seguro en cualquier motor.

Chromium ha ido ampliándolo, y conviene saberlo por dos razones: porque explica que algo vaya suave en Chrome y no en otro sitio, y porque no debe convertirse en una suposición. Desde Chromium 89 se componen las animaciones de SVG, que antes obligaban a repintar en cada fotograma, y se componen las transformaciones expresadas en porcentaje. Chromium ha seguido extendiendo el conjunto a propiedades como el color de fondo y el recorte en condiciones concretas. La regla de ingeniería no cambia: escribe para el conjunto garantizado y considera lo demás un extra que puede estar o no.

Las condiciones sobre el efecto

Aunque la propiedad sea de las buenas, el motor descarta la promoción si el efecto tiene alguna de estas características. Todas ellas son comprobables a simple vista en tu código.

Todas las propiedades del efecto tienen que ser componibles. Esta es la condición que más veces se incumple sin darse cuenta, y su consecuencia es desproporcionada: si en el mismo conjunto de fotogramas animas transform y width, la animación entera va al hilo principal. No se parte en dos. La corrección es separar en dos animaciones distintas, y entonces la de transform sí se compone.

/* Toda la animacion al hilo principal por culpa de height. */
@keyframes mal {
  from { translate: 0 -20px; height: 0; }
  to   { translate: 0 0;     height: 200px; }
}

/* Dos animaciones: la primera se compone, la segunda no. */
@keyframes entra   { from { translate: 0 -20px; } to { translate: 0 0; } }
@keyframes despliega { from { height: 0; } to { height: 200px; } }

El elemento tiene que poder tener capa propia. Un elemento en línea no reemplazado no acepta transformaciones en absoluto, así que la cuestión ni se plantea. Y un elemento cuyo contenido cambie en cada fotograma no gana nada con la capa, porque hay que rasterizarla de nuevo igualmente.

El tamaño de la caja no puede cambiar durante la animación si la transformación depende de él. Los porcentajes en translate y en transform-origin se resuelven contra el tamaño de la caja. Chromium resolvió el caso general precalculando los valores absolutos al empezar, tal y como haría si los hubieras escrito en píxeles. Pero ese precálculo solo es válido si el tamaño no cambia; si algo hace que la caja cambie de tamaño mientras la animación corre, el precálculo deja de servir y la animación se degrada. Por eso una animación de porcentajes sobre un elemento que además responde a un contenedor que se está redimensionando puede comportarse de forma distinta en cada ejecución.

No puede haber otra animación no compuesta sobre la misma propiedad del mismo elemento. Si dos animaciones compiten por transform y una de ellas no cumple las condiciones, ninguna se compone, porque el resultado final depende de ambas.

El elemento no puede necesitar leer lo que hay detrás. mix-blend-mode distinto de normal obliga a componer contra el fondo, lo que restringe fuertemente lo que el compositor puede hacer de forma independiente. Es también la razón por la que backdrop-filter es tan caro pese a estar en la lista de propiedades componibles.

La condición que no está documentada en ninguna tabla: el tiempo tiene que ser predecible

Todas las condiciones anteriores son sobre qué se anima. Falta la que es sobre cómo, y es la que explica la mayoría de los casos donde alguien escribe una animación de transform impecable y aun así no se compone. Para delegar una animación, el compositor necesita poder calcular por sí solo el valor de cada fotograma sin preguntar a nadie. Eso significa que necesita conocer la función completa del tiempo al valor por adelantado: los fotogramas clave, la duración, el retraso, las iteraciones y la curva. Si algo de eso solo se puede saber consultando al hilo principal, la delegación es imposible por construcción, no por falta de soporte. La consecuencia directa, y la razón por la que esta lección existe, es que una animación implementada escribiendo transform en cada callback de requestAnimationFrame nunca se compone, por muy transform que sea la propiedad. No es una animación desde el punto de vista del motor: es una sucesión de estilos en línea. El compositor no tiene ninguna función que evaluar, solo recibe un valor nuevo por fotograma calculado por el hilo principal, y si ese hilo se bloquea no hay valores nuevos. Es exactamente el mismo código, con la misma propiedad, y con un comportamiento categóricamente distinto bajo carga. La versión equivalente con element.animate() sí describe una función, y por eso sí se delega. La regla que resume esto es incómoda porque contradice el instinto de control: cuanto más control tomas sobre cada fotograma, menos puede el navegador ayudarte. La forma de conseguir una animación robusta no es dirigirla tú, es describirla bien y dejar que la ejecute quien puede hacerlo sin ti.

Escribir para cumplirlas por construcción

Cinco decisiones al escribir el código garantizan las condiciones sin tener que comprobarlas después.

Usa las propiedades individuales, no transform. translate, rotate y scale se componen igual y además evitan las colisiones entre animaciones que compartan la propiedad compuesta.

Describe la animación, no la ejecutes. Fotogramas clave en CSS o en element.animate(). Reserva requestAnimationFrame para lo que de verdad necesita cálculo por fotograma, que es mucho menos de lo que parece.

No mezcles propiedades de coste distinto en el mismo efecto. Si necesitas animar altura y desplazamiento a la vez, son dos animaciones.

Evita depender del tamaño durante el movimiento. Si el elemento puede cambiar de tamaño mientras se anima, expresa el desplazamiento en unidades absolutas o en unidades del viewport en lugar de en porcentaje de la caja.

Pide la capa antes, no durante. El patrón de will-change con su retirada, visto en la lección de capas.

Una comprobación rápida que puedes ejecutar sobre cualquier animación antes de darla por buena:

// Devuelve las propiedades del efecto que no pertenecen al
// conjunto componible garantizado en los tres motores.
const COMPONIBLES = new Set([
  'transform', 'translate', 'rotate', 'scale',
  'opacity', 'filter', 'backdropFilter',
  'offset', 'composite', 'easing', 'computedOffset',
]);

function propiedadesNoComponibles(animacion) {
  const vistas = new Set();
  for (const fotograma of animacion.effect.getKeyframes()) {
    for (const clave of Object.keys(fotograma)) {
      if (!COMPONIBLES.has(clave)) vistas.add(clave);
    }
  }
  return [...vistas];
}

for (const a of document.getAnimations()) {
  const malas = propiedadesNoComponibles(a);
  if (malas.length) console.warn('No se compondra por:', malas, a);
}

Esa función no sustituye a mirar la traza —hay condiciones que no se ven en los fotogramas— pero atrapa la causa más frecuente en una línea, y se puede dejar activa en desarrollo.

⚔️ Cumple las condiciones a la fuerza
  1. Escribe una animación que anime translate y height en el mismo conjunto de fotogramas y comprueba en la traza que no se compone.
  2. Sepárala en dos animaciones y comprueba que la de translate sí se compone.
  3. Implementa la misma animación de translate con un bucle de requestAnimationFrame y verifica con el bloqueo del hilo principal que ahora se congela.
  4. Ejecuta la función de comprobación de esta lección sobre las tres versiones y contrasta el resultado con lo que viste en la traza.