will-change y el coste de las capas de compositor
Qué es exactamente una capa, cuánta memoria cuesta, por qué usar will-change mal es peor que no usarlo, y el ciclo correcto de aplicarlo y retirarlo.
will-change es la propiedad que más daño ha hecho por unidad de buena intención. Su promesa —avisar al navegador de lo que va a cambiar para que se prepare— es real y funciona, y su modo de fallo es tan sistemático que en la mayoría de los proyectos donde aparece está empeorando las cosas. La razón es que casi nadie sabe qué paga exactamente al aplicarla, y lo que paga es memoria de GPU, que es el recurso más escaso del dispositivo más modesto de tus usuarios.
- Definir qué es una capa de compositor y calcular su coste en memoria.
- Explicar qué hace
will-changey por qué dejarla puesta anula su beneficio. - Reconocer la explosión de capas y su causa habitual.
- Aplicar el ciclo correcto: promocionar antes, retirar después.
Qué es una capa y qué cuesta
Cuando el navegador decide que un elemento merece una capa de compositor, lo pinta en una textura propia en lugar de pintarlo sobre la superficie común. A partir de ahí puede moverla, escalarla o cambiarle la opacidad sin repintar nada: solo recompone texturas, que es una operación de GPU muy barata. Esa es toda la magia de las animaciones de transform y opacity.
El precio es memoria de vídeo, y se calcula así: ancho por alto en píxeles de dispositivo, por cuatro bytes.
Un elemento de 320 por 200 píxeles CSS en una pantalla con densidad 3 ocupa 960 por 600 píxeles de dispositivo, es decir 576.000 píxeles, unos 2,3 megabytes. Un elemento a pantalla completa en un teléfono de 1170 por 2532 píxeles físicos ocupa casi 12 megabytes. Una capa.
Esa cifra explica el modo de fallo por sí sola. Treinta elementos promocionados a pantalla parcial en un teléfono agotan la memoria de textura disponible, y cuando eso ocurre el sistema no falla de forma limpia: empieza a descartar y recrear texturas, la aplicación se vuelve entrecortada, el navegador puede degradar la aceleración por hardware, y en el peor caso la pestaña se recarga sola. El síntoma que reporta el usuario nunca es “se ha quedado sin memoria de GPU”: es “la web va lenta en mi móvil”.
Además de la memoria, hay dos costes menores pero reales. Cada capa hay que rasterizarla y subirla a la GPU la primera vez, lo que produce un tirón en el momento de la promoción. Y componer muchas capas superpuestas consume tasa de relleno, que en integrados modestos es un límite real.
Qué hace will-change exactamente
will-change le dice al navegador que una propiedad va a cambiar pronto, para que pueda preparar la optimización correspondiente antes de que el cambio ocurra. En el caso de transform, opacity y filter, esa preparación es promocionar el elemento a una capa.
El motivo por el que existe es concreto y merece contarse: sin ella, el navegador descubre que un elemento se anima en el primer fotograma de la animación, y ahí tiene que promocionarlo, rasterizar la textura y subirla a la GPU mientras la animación ya ha empezado. El resultado es un salto en el arranque. Con will-change, ese trabajo se hace antes y la animación arranca limpia.
/* mal: permanente, en la hoja de estilos */
.tarjeta { will-change: transform; }
/* mejor: solo mientras hace falta */
.tarjeta { transition: transform 200ms ease-out; }
.lista:hover .tarjeta { will-change: transform; }
.tarjeta:hover { transform: translateY(-4px); }
En la segunda versión, la promoción ocurre cuando el puntero entra en la lista —antes de que la tarjeta concreta se anime— y se retira cuando sale. La ventana en la que se paga la memoria es la ventana en la que se usa.
Los dos modos de fallo
Por qué usarla mal es peor que no usarla
Hay tres razones, y las tres son consecuencia de lo mismo: will-change desactiva las heurísticas del navegador.
El navegador ya sabe hacer esto. Cuando detecta una animación de transform, promociona el elemento por su cuenta. Y —esto es lo decisivo— lo despromociona cuando la animación termina, liberando la memoria. Un will-change permanente en la hoja de estilos convierte una optimización temporal y bien gestionada en una reserva de memoria que no se libera nunca.
La memoria se paga aunque no se use. Un will-change: transform en una clase que se aplica a doscientos elementos de una lista reserva doscientas capas. Si el usuario nunca pasa el ratón por ninguna, la memoria estaba reservada igual, durante toda la sesión.
Crea contexto de apilamiento y bloque contenedor. will-change: transform tiene los mismos efectos secundarios que transform: el elemento se convierte en bloque contenedor de sus descendientes posicionados de forma fija o absoluta, y crea un contexto de apilamiento. Es decir, una propiedad puesta “por si acaso mejora el rendimiento” puede romperte un elemento fijo o un z-index. Es un bug particularmente difícil de diagnosticar porque nadie sospecha de una declaración que se puso para optimizar.
* { will-change: transform }, o su versión más frecuente y solo un poco menos grave, aplicarlo a todas las tarjetas, todas las filas o todos los elementos animables de un componente. Si encuentras will-change en una regla que coincide con más de un puñado de elementos a la vez, bórralo y mide: en la inmensa mayoría de los casos el rendimiento mejora al quitarlo.
La explosión de capas
Hay un segundo mecanismo que genera capas sin que tú las pidas, y conviene conocerlo porque explica perfiles desconcertantes.
El compositor tiene que preservar el orden de pintado. Si un elemento que estaría por encima de un elemento compuesto se solapa con él, el navegador no puede dejarlo en la superficie común, porque entonces se pintaría por debajo. La solución es promocionar también ese elemento. Y si ese arrastra a otro, sigue.
El resultado es que promocionar un elemento puede promocionar en cadena a todo lo que lo solape por encima. Un banner fijo con una animación en la parte superior de la página puede acabar promocionando media interfaz según el usuario hace scroll y las cosas se cruzan con él.
Los motores modernos han reducido bastante este efecto con arquitecturas de composición más finas, pero el principio sigue en pie: el número de capas no es el número de elementos que tú has promocionado. Se comprueba con la herramienta de capas del navegador, que las lista todas, indica su tamaño en memoria y —lo más útil— el motivo por el que existe cada una.
El ciclo correcto
Para animaciones controladas desde JavaScript, el patrón completo es:
function animarPanel(panel) {
panel.style.willChange = 'transform';
// dejar un fotograma para que la promocion ocurra
requestAnimationFrame(() => {
panel.style.transform = 'translateX(0)';
});
panel.addEventListener('transitionend', () => {
panel.style.willChange = 'auto';
}, { once: true });
}
Tres reglas que resumen todo lo anterior:
Aplícalo poco antes, no siempre. Lo suficiente para que el navegador tenga tiempo de preparar la capa, que en la práctica es un fotograma o el tiempo que tarda el usuario en llegar al elemento.
Retíralo al terminar. Si no lo retiras, has convertido una optimización en una fuga.
Si no puedes retirarlo, no lo pongas. Para un elemento que anima constantemente —un indicador de carga, un fondo animado— el navegador lo mantendrá promocionado por su cuenta y will-change no aporta nada.
Y una comprobación honesta antes de usarlo: mide sin él primero. En la mayoría de las animaciones de interfaz, las heurísticas del navegador ya hacen lo correcto y el tirón de arranque que will-change evita no existe. Es una herramienta para un problema concreto y observado, no una capa de rendimiento que se añade por defecto.
Aquí hay algo que conviene entender bien porque explica su comportamiento anómalo y también por qué tanta gente la usa mal sin darse cuenta. Todas las demás propiedades de CSS son declarativas en el sentido estricto: describen un estado deseado, el motor lo produce, y el resultado no depende de cuándo lo declaraste. color: red significa lo mismo escrito en la hoja o aplicado en el momento. will-change no es eso: es una afirmación sobre el futuro inmediato —“esto va a cambiar pronto”— y su valor depende por completo de que sea cierta y de que siga siéndolo poco tiempo. Una predicción permanente no es una predicción, es una reserva; y una reserva de un recurso finito que nadie libera es, por definición, una fuga. De ahí sale la anomalía que desconcierta a todo el mundo la primera vez: es la única propiedad de CSS cuyo uso correcto exige quitarla, y la única cuyo efecto empeora cuanto más la usas. Hay una lectura más general que merece la pena llevarse. Cada vez que una API te deja darle una pista al sistema sobre lo que va a pasar —precarga de recursos, sugerencias de conexión, prioridades de descarga, reserva de memoria— estás firmando la misma clase de contrato: el sistema deja de decidir por sí mismo y hace lo que tú le has dicho. Eso es una mejora exacta si tu predicción es mejor que su heurística, y una degradación garantizada si no lo es. Como las heurísticas de los navegadores llevan quince años afinándose contra la web real y tu predicción se escribió el martes, la postura por defecto correcta es no dar la pista, medir, y darla solo donde el perfil demuestre que el motor se está equivocando.