wandres.dev
RENDIMIENTO · Medir el jank de verdad

Las capas del compositor y lo que cuestan en memoria

Qué es exactamente una capa de composición, qué la crea incluyendo la composición implícita que no pediste, cómo se calcula su coste en memoria de vídeo, y cómo diagnosticar una explosión de capas.

⏱ 20 min

Promocionar un elemento a su propia capa es el truco más conocido del rendimiento de animación y el más abusado. Una capa es una caché: convierte el trabajo de repintar en el trabajo de mover una textura ya pintada, y como toda caché tiene un coste de memoria y un coste de invalidación que nadie mira hasta que la página se cae en un móvil. Este es el nivel donde hay que hacer las cuentas.

🎯 Al terminar esta lección sabrás
  • Explicar qué es una capa de composición y qué operación abarata exactamente.
  • Enumerar lo que crea capas, incluida la composición implícita por solapamiento.
  • Calcular el coste en memoria de una capa a partir de su tamaño y la densidad de píxel.
  • Diagnosticar una explosión de capas con el panel de capas y los superpuestos de renderizado.

Qué es una capa

El compositor divide la página en superficies que rasteriza por separado y luego combina para formar la imagen final. Cada una de esas superficies es una capa: una textura en memoria de la GPU con el contenido ya dibujado.

Lo que abarata es muy concreto. Sobre una capa ya rasterizada, el compositor puede aplicar una transformación afín y una opacidad sin volver a dibujar nada: mover, escalar, rotar y desvanecer son operaciones que la GPU hace sobre la textura existente. Por eso animar transform y opacity sobre un elemento con capa propia no toca el hilo principal y sobrevive a que ese hilo esté bloqueado.

Lo que no abarata: cualquier cambio del contenido de la capa. Si el texto cambia, si cambia un color, si cambia el tamaño del elemento, la capa se invalida y hay que rasterizarla otra vez. Una capa es una caché del dibujo, y solo sirve si el dibujo no cambia.

De ahí sale la regla que decide si promocionar merece la pena: promociona lo que se mueve y no cambia por dentro. Un panel que se desliza, sí. Un contador que cambia de número mientras se desliza, no: cada cambio de cifra invalida la textura y estás pagando la memoria sin obtener el ahorro.

Qué crea una capa

Explícitamente:

  • will-change: transform, opacity o filter.
  • Una transformación 3D, incluido el viejo truco de translateZ(0) o translate3d(0,0,0).
  • Una animación de transform u opacity que el compositor puede ejecutar por su cuenta.
  • <video>, <canvas> con contexto, contenido WebGL o WebGPU.
  • backdrop-filter.
  • position: fixed en ciertas condiciones, y elementos que se pegan con position: sticky.
  • mix-blend-mode distinto de normal, en algunos casos.

Y luego está la que no pediste y explica la mitad de los problemas: la composición implícita por solapamiento. Si un elemento se pinta por encima de otro que tiene capa propia, y se solapan, ese elemento tiene que tener capa propia también. La razón es de orden de pintado: el compositor dibuja las capas en su orden, y si el elemento de arriba no tuviera capa quedaría por debajo de la textura de la capa promocionada, invirtiendo el orden visual. La única forma de conservar el orden correcto es promocionarlo.

La consecuencia práctica es una cascada. Promocionas una tarjeta que se mueve; sobre esa tarjeta hay un menú desplegable, una insignia y un tooltip; los tres se solapan con ella en algún momento y los tres acaban con capa propia. Y si alguno de ellos se solapa a su vez con otra cosa, sigue. Es lo que se conoce como explosión de capas, y no aparece en tu código en ninguna parte.

/* Esto promociona una tarjeta... */
.tarjeta { will-change: transform; }

/* ...y esto, sin que nadie lo escribiera, promociona todo lo que quede
   encima y se solape: la insignia, el menu, el tooltip, el foco visible. */
⚠️
will-change tiene efectos secundarios más allá del rendimiento

will-change: transform crea un contexto de apilamiento y convierte al elemento en bloque contenedor de sus descendientes con position: fixed y position: absolute. Un descendiente fijo dentro de un elemento con will-change: transform deja de ser fijo respecto al viewport y pasa a serlo respecto a ese elemento. Es la causa de una clase entera de bugs de posicionamiento que nadie relaciona con una propiedad de rendimiento.

Lo que cuesta en memoria

El cálculo es directo. Una capa es una textura con cuatro bytes por píxel, y el número de píxeles es el tamaño en píxeles CSS multiplicado por la densidad de píxel al cuadrado.

memoria ≈ ancho_css × alto_css × dpr² × 4 bytes

Tres ejemplos con números reales:

Una capa a pantalla completa en un portátil. 1440 × 900 CSS con devicePixelRatio 2: 2880 × 1800 = 5.184.000 píxeles × 4 bytes ≈ 20,7 MB.

La misma a pantalla completa en un móvil. 390 × 844 CSS con densidad 3: 1170 × 2532 = 2.962.440 × 4 ≈ 11,8 MB.

Cuarenta tarjetas de una rejilla. 300 × 200 CSS cada una, densidad 2: 600 × 400 = 240.000 × 4 ≈ 960 KB por tarjeta, 38 MB en total. Y si cada tarjeta tiene una insignia que se solapa y se promociona por composición implícita, sube más.

Ese último caso es el que hunde móviles. Treinta y ocho megabytes de memoria de vídeo por una rejilla que se ve bonita al pasar el ratón es un presupuesto que muchos dispositivos no tienen, y cuando se agota el navegador empieza a descartar y a re-rasterizar texturas, que es exactamente el trabajo que las capas iban a evitar.

Dos matices que hacen el cálculo un poco menos brutal en la práctica, sin cambiar la conclusión. Chromium divide las capas grandes en teselas y solo mantiene rasterizadas las que están cerca de la zona visible, lo que reduce el coste de las capas enormes. Y en algunos casos puede usar formatos de menos bits por píxel para contenido sin transparencia. Aun así, el orden de magnitud del cálculo se sostiene y es el que hay que usar para decidir.

Y hay un coste adicional que no es memoria: rasterizar cada capa nueva. Promocionar cuarenta tarjetas no solo reserva memoria, obliga a dibujar cuarenta texturas, y eso ocurre en el hilo del rasterizador con un coste proporcional al área. En el fotograma en el que aparecen todas, ese trabajo se nota.

Diagnosticar

El panel de capas. En Chromium, Más herramientas, Layers. Muestra la página en tres dimensiones con una losa por capa. Al seleccionar una, el detalle da su tamaño en píxeles, la estimación de memoria y —lo más valioso— las razones de composición: por qué existe esa capa. Cuando ves “solapamiento con otra capa compuesta” en veinte losas, ya tienes el diagnóstico.

Los bordes de capa. En el panel Rendering, Layer borders. Dibuja un contorno naranja alrededor de cada capa de composición y una cuadrícula de teselas dentro de las grandes. Es la forma más rápida de ver la explosión: interactúa con la página y observa cuántos contornos aparecen y desaparecen.

El contador de fotogramas y memoria de GPU. En el mismo panel, Frame rendering stats. Muestra los fotogramas por segundo y una estimación de memoria de GPU en uso. Si ese número crece con cada interacción y no baja, estás creando capas que nunca se liberan.

Las tres correcciones, en orden de eficacia:

Quita el will-change que no está en uso. La propiedad está pensada para avisar al navegador poco antes de que algo vaya a cambiar, no para dejarla puesta en la hoja de estilos para siempre. Ponla al empezar la interacción y quítala al terminar.

tarjeta.addEventListener("pointerenter", () => {
  tarjeta.style.willChange = "transform";
});

tarjeta.addEventListener("pointerleave", () => {
  // Espera a que termine la transicion antes de soltar la capa.
  tarjeta.addEventListener("transitionend", () => {
    tarjeta.style.willChange = "auto";
  }, { once: true });
});

Rompe el solapamiento. Si una insignia se promociona porque se solapa con una tarjeta promocionada, a menudo basta con reordenar el contexto de apilamiento o con dar a la insignia un z-index que la coloque por debajo de la capa en el orden de pintado, si el diseño lo permite. En una rejilla, promocionar el contenedor que se mueve en lugar de cada tarjeta elimina la cascada entera.

No promociones lo que cambia por dentro. Vale la pena repetirlo porque es el error más caro: una capa cuyo contenido se redibuja no ahorra nada y sí consume memoria.

Una capa es una caché, y las cachés se evalúan por su tasa de acierto, no por su existencia

La conversación sobre capas se plantea casi siempre en términos morales —promocionar es bueno, no promocionar es malo— cuando es una decisión de caché idéntica a cualquier otra que hayas tomado en tu vida profesional, y por tanto se evalúa con las mismas preguntas. Una caché merece la pena si el coste de mantenerla es menor que el trabajo que ahorra, y eso depende de dos números: cuánto cuesta guardar la entrada y cuántas veces se acierta antes de invalidarla. Para una capa, guardar la entrada cuesta memoria de vídeo, que en un móvil es un recurso escaso y compartido con todo lo demás, y cuesta rasterizarla la primera vez. Acertar significa que el compositor puede reutilizar la textura sin redibujar, lo que ocurre en cada fotograma en el que solo cambien la transformación o la opacidad. Con esos dos números el cálculo se hace solo: un panel que se desliza durante trescientos milisegundos a sesenta fotogramas acierta dieciocho veces seguidas y luego se puede tirar; una tarjeta que hace un efecto de hover mientras su contenido cambia acierta cero veces, porque cada cambio la invalida; y una hoja de estilos con will-change: transform puesto en una clase que aplica a cuarenta elementos mantiene cuarenta entradas en caché durante toda la sesión, aciertan solo durante las milésimas en que algo se mueve, y el resto del tiempo son treinta y ocho megabytes de memoria de vídeo ocupada para nada. Ver will-change como lo que es —una directiva de gestión de caché con alcance manual y sin desalojo automático— explica de golpe por qué la especificación insiste tanto en que se use como último recurso y en que se quite cuando ya no hace falta, y explica también por qué el consejo popular de “ponlo en todo lo que se mueva” produce sistemáticamente páginas más lentas en los dispositivos donde importaba.