Bordes de capa: ver el modelo de composición del navegador
Qué es una capa de composición, qué la crea, cómo se leen los bordes del overlay, y por qué promover todo a capa propia empeora las cosas.
Debajo del árbol del documento hay otra estructura que casi nadie ve y que decide qué se puede animar sin coste y qué no: el conjunto de capas de composición. Cada capa es una textura independiente en memoria de la GPU que se puede mover, escalar y hacer transparente sin repintar nada. Entender qué crea capas, cuántas hay y cuánto ocupan es la diferencia entre una animación que va a sesenta fotogramas por segundo con el hilo principal bloqueado y otra que se atasca sin motivo aparente.
- Explicar qué es una capa de composición y qué operaciones permite hacer gratis.
- Enumerar las causas que promueven un elemento a capa propia.
- Leer los bordes del overlay y distinguir una capa de una región de baldosas.
- Justificar por qué crear muchas capas degrada el rendimiento en lugar de mejorarlo.
Qué es una capa y qué compra
El navegador no dibuja la página como una sola imagen. La divide en capas, dibuja cada una por separado, las sube a la GPU como texturas, y compone el resultado final combinándolas. Ese último paso, la composición, es extraordinariamente barato porque es exactamente para lo que sirve una GPU.
La consecuencia práctica es la regla más importante del rendimiento de animación: si un cambio se puede expresar como una transformación o un cambio de opacidad de una capa existente, no hace falta repintar nada. El navegador reutiliza la textura que ya tiene y solo cambia cómo la coloca o la mezcla. Eso puede hacerlo el hilo del compositor por su cuenta, incluso con el hilo principal completamente bloqueado.
Y la contrapartida, igual de importante: cualquier cambio que no se pueda expresar así obliga a repintar la capa y a volver a subirla a la GPU, y entonces el hilo principal vuelve a estar en el camino crítico.
Esto explica una observación desconcertante: una animación de transformación sigue moviéndose fluida mientras un bucle de JavaScript bloquea la página durante dos segundos, y una animación de la propiedad de posición se congela en seco. No es que una propiedad sea más rápida que otra: es que una la ejecuta un hilo distinto y la otra no.
Qué crea una capa
La lista de causas no está estandarizada y varía entre motores, pero el núcleo es estable:
Un elemento con una transformación tridimensional o con will-change declarando que va a cambiar la transformación o la opacidad. Un vídeo o un lienzo con contexto acelerado. Un elemento con una animación en curso de transformación u opacidad. Un elemento con posición fija, en muchos casos. Un elemento con ciertos filtros o con mezcla que la GPU tiene que resolver. Y, de forma indirecta, cualquier elemento que se solape con otro que ya está en su propia capa y esté por encima en el orden de apilamiento.
Esa última causa es la que produce sorpresas y merece explicación. Si un elemento tiene su propia capa y otro elemento se pinta encima de él, el navegador no tiene más remedio que darle también capa propia al de arriba, porque de otro modo no podría respetar el orden de apilamiento al componer. Promover un elemento puede provocar en cascada la promoción de varios otros que se solapan con él, y ese efecto multiplicador es la causa habitual de que un sitio acabe con centenares de capas sin que nadie lo pidiera.
Leer el overlay
La casilla correspondiente del panel de renderizado dibuja los límites de las capas y de las baldosas en que se dividen.
Los bordes de capa rodean cada capa de composición. Ver muchos rectángulos por toda la página es la señal de alarma.
Los bordes de baldosa subdividen capas grandes. Una capa muy grande no se sube a la GPU de una pieza: se trocea en baldosas que se rasterizan por separado y bajo demanda. Ver la subdivisión no es un problema, es el funcionamiento normal.
Lo que se busca con este overlay son tres cosas: capas que no deberían existir, típicamente elementos con will-change puesto de forma preventiva y olvidado; capas enormes, que consumen memoria proporcional a su superficie en píxeles reales; y capas que se crean y se destruyen continuamente, que es lo peor de todo porque cada creación implica un pintado completo y una subida a la GPU.
Por qué promover todo es un error
La promoción a capa parece una optimización gratuita y no lo es: tiene tres costes concretos.
Memoria de GPU. Cada capa es una textura. Su tamaño en memoria es ancho por alto en píxeles físicos por bytes por píxel. Una capa a pantalla completa en un móvil con densidad triple ocupa varios megabytes. Cien capas pueden agotar la memoria de vídeo de un dispositivo modesto, y cuando eso ocurre el navegador empieza a descartar y a rehacer texturas, con un coste muchísimo mayor que el que se pretendía evitar.
Trabajo de composición. Componer capas es barato por capa, no gratis. Con un número grande, el propio trabajo de composición se convierte en el cuello de botella, y entonces la pista de la GPU en el perfil está llena mientras el hilo principal está vacío.
Efectos secundarios visuales. Una capa propia puede cambiar el redondeo de subpíxeles del texto, provocando que la tipografía se vea ligeramente distinta, y en algunos casos desactiva el suavizado subpíxel.
La regla operativa es sencilla: promueve solo lo que vas a animar, y solo mientras lo animes.
// Promocion temporal correcta: se pone antes de animar y se quita al terminar
function animarConCapaTemporal(el, keyframes, opciones) {
el.style.willChange = 'transform, opacity';
const animacion = el.animate(keyframes, opciones);
animacion.finished
.catch(() => {})
.finally(() => { el.style.willChange = 'auto'; });
return animacion;
}
// Ejemplo ejecutable
const caja = document.createElement('div');
caja.style.cssText = 'width:80px;height:80px;background:#89b4fa;border-radius:12px';
document.body.append(caja);
animarConCapaTemporal(caja,
[{ transform: 'translateX(0)' }, { transform: 'translateX(240px)' }],
{ duration: 900, iterations: 2, direction: 'alternate', easing: 'ease-in-out' }
);
El detalle que importa es la limpieza en el bloque final: dejar will-change puesto de forma permanente es exactamente el error que produce sitios con cientos de capas. La propiedad es una promesa sobre el futuro inmediato, no una declaración sobre la naturaleza del elemento.
Contar capas y su coste
El perfil de rendimiento incluye la información de capas cuando se graba con esa opción activada, y el panel de estadísticas de fotogramas muestra la memoria de GPU en uso. Para una comprobación rápida sin grabar nada, esta aproximación identifica candidatos:
// Inventario de elementos que probablemente tengan capa propia
(() => {
const razones = [];
for (const el of document.querySelectorAll('*')) {
const s = getComputedStyle(el);
const motivos = [];
if (s.willChange !== 'auto') motivos.push('will-change: ' + s.willChange);
if (s.transform !== 'none' && /matrix3d|translate3d|translateZ/.test(s.transform))
motivos.push('transformacion 3D');
if (s.position === 'fixed' || s.position === 'sticky') motivos.push(s.position);
if (s.filter !== 'none') motivos.push('filtro');
if (s.backdropFilter && s.backdropFilter !== 'none') motivos.push('filtro de fondo');
if (s.mixBlendMode !== 'normal') motivos.push('mezcla');
if (el.tagName === 'VIDEO' || el.tagName === 'CANVAS') motivos.push(el.tagName.toLowerCase());
if (!motivos.length) continue;
const r = el.getBoundingClientRect();
const dpr = devicePixelRatio || 1;
razones.push({
elemento: el.tagName.toLowerCase() + (el.id ? '#' + el.id : ''),
motivos: motivos.join(', '),
anchoCSS: Math.round(r.width),
altoCSS: Math.round(r.height),
mbAproxGPU: +((r.width * dpr) * (r.height * dpr) * 4 / 1048576).toFixed(2)
});
}
razones.sort((a, b) => b.mbAproxGPU - a.mbAproxGPU);
console.table(razones.slice(0, 30));
console.log('Candidatos a capa:', razones.length,
'| MB aproximados totales:', razones.reduce((s, r) => s + r.mbAproxGPU, 0).toFixed(1));
})();
La estimación de memoria es aproximada y suficiente para lo que importa: si la suma sale de decenas o cientos de megabytes, hay un problema de capas en un dispositivo modesto aunque en tu equipo no se note.
La razón profunda por la que este tema merece una lección propia no es la optimización de animaciones sino algo más estructural: la separación entre el hilo principal y el compositor es el único mecanismo del navegador que permite que la interfaz siga viva cuando el JavaScript no responde. El desplazamiento, que es la interacción más frecuente de la web, lo maneja el compositor por su cuenta siempre que pueda; por eso una página con un bucle infinito en el hilo principal a veces se puede seguir desplazando. Las animaciones de transformación y opacidad, igual. Y esa capacidad se pierde en cuanto el hilo principal tiene que participar, cosa que ocurre por razones muy concretas y evitables: un manejador de rueda o de toque no pasivo, porque el compositor no puede saber si el manejador va a cancelar el evento y tiene que esperar a preguntar; un efecto que depende de la posición de desplazamiento y se calcula en JavaScript; una animación de una propiedad que no sea de composición. Cada una de esas decisiones devuelve el desplazamiento al hilo principal y con ello lo somete a todo lo que allí ocurra. La consecuencia de diseño es potente y poco conocida: hay una clase entera de aplicaciones cuya interfaz se siente sólida bajo carga y otra cuya interfaz se congela, y la diferencia casi nunca está en cuánto trabajo hacen sino en si su interacción principal depende del hilo principal o no. Por eso las tres reglas que más impacto tienen sobre la sensación de solidez son de esta naturaleza y no de optimización: manejadores de desplazamiento y de toque siempre pasivos salvo que de verdad tengan que cancelar el evento; efectos ligados al desplazamiento expresados con animaciones dirigidas por scroll cuando estén disponibles, o con un observador de intersección, en lugar de con un manejador que lee posiciones; y animaciones limitadas a transformación y opacidad siempre que exista una forma de expresarlas así. Ninguna de las tres reduce el trabajo total de la aplicación en un solo milisegundo; las tres cambian quién lo hace, y eso es lo que el usuario percibe.