wandres.dev
RENDERIZADO I · El pipeline de píxeles

Capas de composición: qué son, qué cuestan y cuándo promocionar

Qué convierte a un elemento en capa propia, cuánta memoria cuesta cada una, por qué will-change en una hoja de estilos es una mala idea, y cómo se produce una explosión de capas sin haberla pedido.

⏱ 18 min

La última etapa del pipeline trabaja con capas: superficies rasterizadas de forma independiente que la GPU puede mover, escalar y mezclar sin repintar nada. Promocionar un elemento a capa propia es lo que convierte una animación cara en una gratis, y es también la optimización que más veces he visto aplicada de forma indiscriminada hasta convertirse en el problema que pretendía resolver. La memoria de vídeo es finita y cada capa se la come.

🎯 Al terminar esta lección sabrás
  • Enumerar los motivos por los que un elemento acaba teniendo capa propia.
  • Calcular la memoria que consume una capa a partir de su tamaño y la densidad de píxeles.
  • Aplicar will-change con el ciclo de vida correcto en lugar de dejarlo puesto.
  • Diagnosticar una explosión de capas por solapamiento y corregirla.

Qué es una capa y por qué existe

Una capa de composición es una superficie rasterizada por separado: su propio mapa de bits, con su propia textura en la GPU. El compositor tiene una lista de capas con una transformación y una opacidad cada una, y produce el fotograma dibujándolas en orden.

La ventaja aparece cuando algo cambia. Si el elemento que se mueve tiene su propia capa, mover la capa es actualizar una matriz de cuatro por cuatro y volver a dibujar: no hay que repintar ni rasterizar nada. Si no la tiene, forma parte de la textura de otra capa, y moverlo obliga a regenerar esa textura entera.

Los motivos por los que un elemento acaba con capa propia, ordenados de más explícito a más sorprendente:

Lo has pedido. will-change: transform, will-change: opacity o will-change: filter. Es la forma declarativa y moderna de pedirlo.

Tiene una transformación tridimensional. transform: translateZ(0) o transform: translate3d(0,0,0). Es el truco antiguo, anterior a will-change, y sigue funcionando por compatibilidad. No lo uses en código nuevo: dice lo que quieres conseguir en lugar de por qué.

Tiene una animación gestionada por el motor sobre una propiedad compuesta. Una transición o una animación de CSS, o una animación de la API web, sobre transform u opacity. El motor promociona mientras dura y despromociona al terminar, que es el comportamiento ideal.

Es un elemento que se rasteriza aparte por naturaleza. Un vídeo, un lienzo con contexto tridimensional, un iframe compuesto aparte.

Tiene un filtro de fondo. backdrop-filter obliga a componer, porque necesita leer lo que hay debajo.

Está por encima de una capa. Y este es el que produce sorpresas: si un elemento se pinta encima de otro que ya tiene capa propia, el motor tiene que decidir cómo respetar el orden de pintado. La solución habitual es promocionarlo también. Se llama composición implícita, y es el mecanismo detrás de la mayoría de las explosiones de capas.

Cuánto cuesta una capa

La cuenta es directa: anchura por altura por cuatro bytes, en píxeles de dispositivo. Y “en píxeles de dispositivo” es la parte que se olvida, porque en un móvil moderno la densidad es de dos o tres, y la memoria escala con el cuadrado.

Capa Densidad 1 Densidad 2 Densidad 3
300 x 200 0,23 MB 0,92 MB 2,06 MB
800 x 600 1,83 MB 7,32 MB 16,5 MB
Pantalla completa 390 x 844 1,26 MB 5,03 MB 11,3 MB

Un panel a pantalla completa promocionado en un móvil de densidad tres cuesta once megabytes de memoria de vídeo. Treinta tarjetas de 300 por 200 promocionadas en el mismo dispositivo cuestan sesenta.

Cuando la memoria de vídeo disponible se agota, el navegador no falla con un error: degrada. Deja de promocionar, cae a rasterizado por software en algunos casos, o descarta y regenera texturas continuamente. El síntoma es una página que va bien un rato y después empieza a dar tirones sin razón aparente, y es especialmente difícil de diagnosticar porque el perfil del hilo principal está limpio.

Además del coste de memoria hay un coste por fotograma: el compositor tiene que recorrer y dibujar todas las capas en cada fotograma. Con diez capas, eso es una fracción de milisegundo. Con doscientas, se convierte en varios milisegundos y en un consumo de energía notable. Más capas no es mejor; hay un óptimo y está bajo.

La inspección se hace en el panel de capas de las herramientas de desarrollo, que lista cada capa con su tamaño, su consumo de memoria estimado y el motivo por el que existe. Esa última columna es la que resuelve las explosiones, porque distingue las capas que pediste de las que aparecieron solas.

Y la superposición de bordes de capa, en el panel de renderizado, dibuja el contorno de cada una sobre la página. Es la forma más rápida de ver de un vistazo si tienes cinco capas o ciento cincuenta.

will-change y su ciclo de vida

will-change es una promesa al navegador: “esta propiedad va a cambiar, prepárate”. El navegador la cumple reservando la capa desde el momento en que la declaración se aplica y mientras siga aplicándose.

De ahí sale la regla que se incumple casi siempre: will-change en una hoja de estilos, sobre un selector que empareja con muchos elementos, es memoria reservada permanentemente para animaciones que quizá nunca ocurran.

/* MAL: cincuenta tarjetas con capa propia desde que carga la pagina */
.tarjeta {
  will-change: transform;
}

El ciclo de vida correcto es puntual: se pone justo antes de la animación y se quita al terminar.

function abrirPanel(panel) {
  panel.style.willChange = 'transform, opacity';

  const animacion = panel.animate(
    [
      { transform: 'translateY(16px)', opacity: 0 },
      { transform: 'translateY(0)', opacity: 1 },
    ],
    { duration: 200, easing: 'ease-out', fill: 'forwards' },
  );

  animacion.finished.finally(() => {
    panel.style.willChange = '';      // devolver la memoria
  });
}

Y para el caso declarativo, hay una forma de acotarlo con CSS que funciona bien: aplicar will-change solo mientras el elemento está en un estado que anuncia la animación inminente.

.tarjeta { transition: transform 180ms ease-out; }

/* La capa se reserva al acercarse el puntero, no antes */
.lista:hover .tarjeta,
.lista:focus-within .tarjeta {
  will-change: transform;
}

Con eso la memoria se reserva unos milisegundos antes de que haga falta y se libera cuando el puntero se va, que es exactamente el comportamiento que la propiedad fue diseñada para tener.

Un aviso que la especificación hace explícitamente y que conviene repetir: will-change no es un acelerador. Aplicarlo a un elemento que no va a cambiar no lo hace más rápido; lo hace más caro. Y aplicarlo a una propiedad que no es compuesta no consigue nada, porque la promoción a capa no evita el layout.

La explosión de capas por solapamiento: promocionas uno y acabas con ochenta

Este es el fallo que más veces he tenido que diagnosticar en este tema, y tiene la propiedad desagradable de que el código que lo causa parece perfectamente inocente y está a mucha distancia del síntoma.

El mecanismo es el orden de pintado. El navegador tiene que respetar que los elementos se pintan en un orden determinado por el flujo, el apilamiento y el índice de apilamiento. Si un elemento A tiene capa propia y un elemento B se pinta después que A y se solapa con él en pantalla, el compositor no puede simplemente dibujar la capa de A sobre la textura donde vive B, porque quedaría por encima cuando debería quedar por debajo. La solución que aplica el motor es promocionar también a B.

Y ahora la parte que lo convierte en explosión: si B promociona, cualquier elemento que se pinte después y se solape con B tiene el mismo problema. La promoción se propaga por la cadena de solapamientos.

El caso real típico: una cabecera fija que promocionas para que el desplazamiento vaya suave. La cabecera está arriba. Todo lo que pasa por debajo de ella al desplazarse se solapa con ella en algún momento. En una lista larga, eso puede significar que decenas de filas acaben con capa propia, cada una con su textura, en un móvil de densidad tres. Ochenta megabytes de memoria de vídeo por una declaración de dos palabras.

Los síntomas por los que se reconoce, y ninguno apunta al culpable:

  • El desplazamiento va a tirones después de un rato, no desde el principio.
  • El perfil del hilo principal está limpio y aun así se pierden fotogramas.
  • El consumo de batería del dispositivo es notablemente alto en esa pantalla.
  • La superposición de bordes de capa muestra la página cuadriculada.

El diagnóstico es de un minuto: abrir el panel de capas y ordenar por motivo. Si hay una lista larga de capas cuyo motivo es el solapamiento con otra capa, ya está.

Y las tres correcciones, en orden de preferencia.

Uno: sube el elemento promocionado por encima de todo con el índice de apilamiento. Si el elemento con capa se pinta el último, nada se pinta después de él y no hay nada que promocionar en cascada. Una sola declaración resuelve el caso de la cabecera fija:

.cabecera-fija {
  position: fixed;
  z-index: 10;          /* que se pinte la ultima: nada se solapa por encima */
  will-change: transform;
}

Dos: no promociones lo que no necesita promoción. La cabecera fija del ejemplo probablemente no la necesitaba: las posiciones fijas ya las gestiona el compositor en la mayoría de los casos. La promoción manual se añadió como amuleto, sin medir antes y sin medir después.

Tres: si la promoción es imprescindible y el solapamiento también, reduce el área. Una capa de pantalla completa que solo necesita moverse en una franja se puede recortar. Menos superficie es menos memoria y menos rasterizado.

La lección general, que vale más allá de este caso concreto: la promoción a capa es una optimización que hay que medir antes y después, como cualquier otra. Es la única del repertorio que se ha convertido en superstición hasta el punto de aplicarse a ciegas, con transform: translateZ(0) copiado de una respuesta de foro de 2013 y pegado en la clase base de un sistema de diseño. Si no puedes enseñar el perfil de antes y el de después, quítalo.

El resumen operativo

Promociona cuando un elemento se va a animar con propiedades compuestas de forma sostenida, y lo has medido.

No promociones cuando el elemento no se anima, cuando la animación es de una propiedad que provoca layout, o cuando el selector empareja con más de un puñado de elementos.

Quita la promoción en cuanto la animación termina, con la promesa de la animación o con una clase que se retira.

Vigila el recuento total. Como cifra orientativa, por debajo de veinte capas no hay conversación que tener; por encima de cien en un móvil, hay un problema aunque todavía no se note.

⚔️ Reto práctico

Abre el panel de capas en la pantalla más pesada de tu aplicación y anota tres cifras: el número de capas, la memoria total estimada y cuántas de ellas tienen como motivo el solapamiento. Después busca en tu hoja de estilos todas las apariciones de will-change y de translateZ y comprueba, una por una, si alguien midió antes de ponerlas. Quita las que no puedas justificar y vuelve a mirar las tres cifras.