VanguardIA®

Ingeniería

Animar con GSAP sin castigar el rendimiento

Una web animada no va lenta por tener muchas animaciones. Va lenta por animar las propiedades equivocadas, y eso se arregla sabiendo cuáles son.

6 min de lectura

Casi todo el mal rendimiento que se atribuye a las animaciones no viene de animar demasiado. Viene de animar lo que no se debe, de disparar cuando no toca y de no haberlo mirado nunca en un teléfono de verdad.

Esto es lo que aplicamos en producción, con las razones. No hay nada aquí que no se pueda comprobar en veinte minutos con las herramientas del navegador.

1 · Anima transform y opacity. Lo demás, casi nunca

El navegador pinta un fotograma en cinco fases: estilo, layout, pintado, composición y presentación. Cada propiedad entra por una fase distinta, y ahí está toda la diferencia.

Si animas…DisparaCoste
width, height, top, left, margin, paddingLayout → pintado → composiciónAlto, y crece con el número de nodos
background-color, box-shadow, border-radiusPintado → composiciónMedio, sube con el área
transform, opacity, filterSólo composiciónBajo, lo lleva la GPU

Un left: 0 → 400px obliga al navegador a recalcular la posición de todo lo que pueda haberse desplazado, en cada fotograma. Un x: 400 mueve una textura ya rasterizada. El resultado en pantalla es idéntico y el coste no se parece.

Regla de bolsillo: si lo que animas puede cambiar de sitio a un elemento vecino, va a costar caro.

GSAP ayuda aquí más de lo que parece: x, y, scale y rotation se escriben todas al mismo transform, con orden estable y sin que tengas que concatenar cadenas. Eso evita el error clásico de dos tweens pisándose la matriz.

2 · Mide desde el layout, no desde el tween

Hay animaciones que no se pueden hacer sólo con transform: una tarjeta que crece de miniatura a pantalla completa cambia de tamaño de verdad. Para eso está FLIP, que es la técnica de medir el estado inicial (First), aplicar el final (Last), calcular la diferencia (Invert) y animar sólo esa diferencia con transform (Play).

El resultado es una animación que parece cambiar de tamaño mientras el navegador sólo compone. Un cambio de caja se convierte en un cambio de matriz.

3 · Un ScrollTrigger por gesto, no uno por elemento

Es tentador poner un disparador a cada tarjeta de una rejilla de cuarenta. Cada uno mantiene su propio cálculo de posiciones y todos se recalculan cuando el documento cambia de alto.

Un solo disparador sobre el contenedor, con stagger hacia dentro, hace lo mismo con una cuadragésima parte de la contabilidad:

4 · Un refresh() cuando el documento acabe de crecer

Este es el fallo que más veces hemos visto en revisiones ajenas, y es invisible en escritorio.

document.fonts.ready espera a las fuentes, no a las imágenes. Si una foto sin width/height llega tarde, el documento cambia de altura después de que los disparadores hayan anotado sus posiciones. A partir de ahí todos apuntan a coordenadas que ya no existen, y el síntoma es siempre el mismo: una animación del final de la página que se queda a medias y que «se arregla sola» si redimensionas la ventana.

Dos medidas, las dos baratas:

  1. Atributos width y height en todas las imágenes, para que el hueco esté reservado desde el primer pintado.
  2. Un ScrollTrigger.refresh() en el evento load, que es el redimensionado que el navegador nunca te va a dar.

5 · Lo que no está en pantalla no debe estar decodificando

Un vídeo en bucle a mitad de una página de nueve mil píxeles sigue decodificando fotogramas mientras nadie lo ve. Con cuatro, el teléfono se calienta y el scroll empieza a saltar — y el culpable aparente será la animación, que es lo que se ve moverse.

El patrón es un IntersectionObserver que haga dos cosas distintas y no las confunda:

Lo mismo vale para cualquier lienzo con WebGL: fuera de pantalla, se para el bucle.

6 · Carga el motor pesado sólo si se va a usar

Si tienes un efecto que sólo existe con ratón —un cursor a medida, un hover con shaders—, un import estático lo mete en el paquete principal para todo el mundo, teléfonos incluidos, donde la condición nunca se cumple. Un import() dinámico detrás del if lo convierte en un trozo aparte que sólo se pide cuando hace falta. En esta página son 33 KB que un móvil no descarga jamás.

7 · prefers-reduced-motion no es una casilla de accesibilidad

Respetarlo bien tiene dos consecuencias que casi nadie aprovecha:

8 · Mídelo en el aparato que importa

Un MacBook a 120 Hz esconde absolutamente todo. El presupuesto real es de 16,7 ms por fotograma a 60 Hz, y de esos el navegador ya gasta unos cuantos por su cuenta.

Lo que miramos antes de dar por buena una página:

  1. Performance con estrangulamiento de CPU ×4 o ×6. Si aparecen barras rojas de layout durante el scroll, hay una propiedad mal elegida.
  2. La capa de Rendering con «Paint flashing»: lo que parpadea es lo que se repinta, y no debería parpadear casi nada mientras haces scroll.
  3. CLS por debajo de 0,1. La mayor parte de las animaciones de entrada mal hechas se pagan aquí.
  4. Un teléfono real, con datos móviles, sin caché.
Si una animación sólo se ve bien en la máquina en la que se hizo, no está terminada.

El resumen, en cinco líneas

Los componentes del catálogo de esta casa están escritos con estas reglas puestas, y cada uno lleva anotado por qué hace lo que hace. Es el mismo criterio, sólo que ya resuelto.