El reloj y la configuración global
El ticker que mueve toda la máquina, el modelo de tiempo estricto que GSAP eligió frente a los demás motores, la suavización del retraso, y las dos formas de configurar el motor entero.
Todo lo que GSAP anima está movido por un único reloj. No hay un requestAnimationFrame por animación: hay uno solo, que avanza una timeline raíz de la que cuelga absolutamente todo, y del que salen todas las actualizaciones en el mismo instante. Esa decisión de arquitectura es la que hace posible pausar el motor entero, escalar su velocidad, tener cien animaciones perfectamente sincronizadas sin coste añadido, y sobrevivir con dignidad a un pico de carga. Conocer el reloj es conocer el modelo de tiempo del que dependen todas las demás lecciones.
- Explicar el modelo de un ticker único y una timeline global, y qué gana frente a un bucle por animación.
- Enganchar código propio al ticker con los parámetros correctos y desengancharlo.
- Describir qué hace la suavización del retraso y por qué GSAP prioriza la sincronización sobre la duración.
- Distinguir
gsap.configdegsap.defaultsy saber qué va en cada uno.
Un reloj, una timeline raíz
gsap.ticker es el latido del motor: escucha requestAnimationFrame y en cada evento actualiza gsap.globalTimeline, que es la timeline raíz donde acaba todo lo que creas sin un padre explícito. Cuando escribes un gsap.to() suelto, no estás creando una animación independiente: estás insertando un hijo en esa timeline global.
Eso tiene consecuencias que se notan enseguida. Cien animaciones no son cien bucles compitiendo: son cien hijos que se actualizan en un único recorrido, en un único frame, con exactamente el mismo valor de tiempo. La sincronización no es algo que tengas que conseguir; es el estado por defecto.
Y da acceso a operaciones que de otro modo no existirían:
// Pausar absolutamente todo lo que GSAP esta animando.
gsap.globalTimeline.pause();
// Camara lenta global: util para depurar una coreografia.
gsap.globalTimeline.timeScale(0.2);
// Volver a la normalidad.
gsap.globalTimeline.timeScale(1).play();
Ese trío es una herramienta de desarrollo por derecho propio, y es la razón por la que depurar una animación de GSAP es cualitativamente distinto de depurar una animación de CSS.
Hay un aviso importante: la timeline global mueve todo, incluidas las llamadas retardadas que hayas programado con gsap.delayedCall. Pausarla congela cosas que quizá no querías congelar. Para el caso concreto de “ralentiza lo que ya hay pero no lo que voy a crear” existe gsap.exportRoot(), que envuelve en una timeline nueva todo lo que hay actualmente en la raíz y te la devuelve, dejando la raíz libre para lo siguiente.
Engancharse al ticker
Cualquier código que tenga que correr una vez por frame debería ir en el ticker de GSAP en lugar de en su propio requestAnimationFrame, por dos razones: se ejecuta después de que GSAP haya actualizado todo, con lo que lees valores ya consistentes, y comparte el mismo bucle en vez de añadir otro.
function porFrame(time, deltaTime, frame) {
// time : segundos totales desde que arranco el ticker
// deltaTime : milisegundos desde el tick anterior
// frame : numero de tick, se incrementa en cada uno
dibujarCanvas(deltaTime);
}
gsap.ticker.add(porFrame);
// Y para quitarlo:
gsap.ticker.remove(porFrame);
add acepta dos parámetros opcionales más: once, que quita el listener automáticamente después de dispararse una vez, y prioritize, que lo pone al principio de la cola en vez de al final para que corra antes de que GSAP actualice la timeline global.
// Correr una sola vez, en el proximo frame, ANTES de que GSAP actualice.
gsap.ticker.add(medirLayout, true, true);
Ese prioritize es la respuesta al patrón de “necesito leer el layout antes de que las animaciones lo modifiquen en este frame”, que resuelto a mano exige coordinar dos bucles.
Para el problema del framerate variable, el ticker expone deltaRatio, que devuelve el tiempo transcurrido como una razón respecto a un framerate objetivo. Si el objetivo son 60 fps y el frame ha durado el doble, devuelve 2:
gsap.ticker.add(() => {
// Avanza a la misma velocidad real sea cual sea el framerate.
objeto.x += 3 * gsap.ticker.deltaRatio(60);
});
Y gsap.ticker.fps(30) limita el ritmo de actualización saltándose ticks. No puede subir por encima de lo que da el navegador —pedirle 100 no acelera nada— pero bajarlo es útil para un fondo animado que no necesita ir a 120 Hz en una pantalla que sí puede.
Cuando no queda ninguna animación activa, el motor apaga el ticker para no consumir batería, y lo vuelve a encender en cuanto creas algo. La frecuencia con la que comprueba si puede dormirse la controla la opción autoSleep de gsap.config, que por defecto son 120 frames, unos dos segundos. Es un detalle que rara vez hay que tocar, pero explica por qué en el perfilador no ves actividad continua con la página quieta.
Cuando el navegador se atasca
Este es el comportamiento más característico del motor y el que más gente desconoce.
Imagina un tween de dos segundos que debería empezar ya, y el hilo principal bloqueado un segundo entero antes de poder renderizar. Hay dos filosofías posibles. Casi todos los motores —incluidas las animaciones de CSS en algunos navegadores— deslizan el instante de inicio hacia delante, de modo que la animación dura sus dos segundos completos pero empieza tarde. GSAP hace lo contrario: mantiene un modelo de tiempo estricto y renderiza el tween como si ya estuviera a la mitad.
La razón es la sincronización. Si cada animación desliza su inicio por su cuenta, un conjunto de animaciones escalonadas que debían salir separadas 100 ms sale en grumos, todas juntas, porque las que estaban esperando arrancan a la vez al desbloquearse el hilo. Ese efecto es muy visible y arruina cualquier coreografía. GSAP prefiere sacrificar la duración individual antes que la relación temporal entre piezas.
El problema del modelo estricto es que un bloqueo largo produce un salto brusco: dos segundos de congelación son dos segundos de animación consumidos de golpe. Para eso existe lagSmoothing, que está activada por defecto:
// Los valores por defecto: si entre dos ticks pasan mas de 500 ms,
// el motor hace como si solo hubieran pasado 33.
gsap.ticker.lagSmoothing(500, 33);
// Desactivarla por completo (rara vez es buena idea).
gsap.ticker.lagSmoothing(0);
El ajuste se aplica al reloj interno, así que afecta a todo por igual y la sincronización se conserva. Un bloqueo de dos segundos hace avanzar las animaciones 33 milisegundos en vez de dos segundos, y la coreografía sigue intacta.
Bajar mucho el umbral es tentador y contraproducente: con valores muy pequeños el reloj se corrige casi en cada frame y las animaciones parecen ir a cámara lenta, porque literalmente lo hacen. Los 500 y 33 por defecto son un buen equilibrio y rara vez merece la pena tocarlos.
Configurar el motor: dos métodos, dos ámbitos
Hay dos funciones de configuración global y confundirlas es habitual.
gsap.defaults() establece valores que heredan los tweens que crees a partir de ese momento. Es para propiedades de animación.
gsap.defaults({
ease: 'power2.out',
duration: 0.6,
});
A partir de ahí, cualquier tween sin ease ni duration explícitos usa esos. Es la forma de imponer el lenguaje de movimiento de un proyecto en un solo sitio, y equivale a lo que en un sistema de diseño serían los tokens de movimiento. Recuerda que el valor de fábrica del ease es power1.out y el de la duración, 0.5.
gsap.config() establece cosas del motor que no son propiedades de un tween:
gsap.config({
autoSleep: 60, // cada cuantos frames comprueba si puede dormirse
force3D: false, // no forzar componente 3D en las transformaciones
nullTargetWarn: false, // no avisar cuando el selector no encuentra nada
units: { left: '%', rotation: 'rad' }, // unidades por defecto por propiedad
});
De esas cuatro, la que más se usa es nullTargetWarn, y conviene pensarlo dos veces: el aviso de objetivo nulo existe porque un selector que no encuentra nada es casi siempre un bug —un componente que aún no se ha montado, una clase renombrada— y silenciarlo globalmente es tapar el detector de humos. La alternativa razonable es dejarlo activo y comprobar los objetivos antes de animar.
units es la menos conocida y resuelve un problema real: por defecto GSAP asume píxeles para casi todo y grados para las rotaciones, así que { left: 100 } va a 100 píxeles. Si tu proyecto trabaja en porcentajes, declararlo una vez evita escribir "100%" como cadena en cada tween.
La integración con un motor de render propio parece un tema avanzado y en realidad es una consecuencia directa de la arquitectura de un solo reloj. Con un bucle de render propio tienes dos relojes independientes: el tuyo y el de la librería de animación. Eso significa que en algunos frames lees valores actualizados y en otros no, y aparecen microtemblores que nadie sabe explicar. Con GSAP no hay dos relojes: pones tu render en gsap.ticker.add y desapareces tu requestAnimationFrame. Tu función corre después de que GSAP haya escrito todos los valores del frame, así que lees estado consistente por construcción, y todo lo que GSAP anime —incluidas propiedades de tus objetos de Three.js— estará listo cuando dibujes. Es una línea de código y elimina de raíz una clase entera de bugs de sincronización. Si algún día heredas un proyecto con requestAnimationFrame propio y animaciones de GSAP que “a veces van un frame por detrás”, este es el arreglo.
Instrumenta el ticker para verlo funcionar: engancha una función que registre deltaTime en un array circular de 300 posiciones y píntalo en un canvas como histograma. Después provoca un bloqueo del hilo principal con un bucle síncrono de un segundo y observa dos cosas: el pico en el histograma y, gracias a lagSmoothing, que tus animaciones no dan el salto que ese pico haría esperar. Repite con gsap.ticker.lagSmoothing(0) y compara.