getAnimations como herramienta de depuración
Inventariar todo lo que se mueve en un documento, congelar el estado para una captura, resolver el timing real de una animación, encontrar quién anima un elemento y estabilizar las pruebas visuales.
getAnimations() se presenta habitualmente como una API de control: pausar todo, invertir, cambiar la velocidad. Su mejor uso es otro. Es la única forma de preguntarle al navegador qué está animando en este instante, con los valores ya resueltos, sin leer una línea de CSS ni suponer nada. Con veinte líneas en la consola tienes un inventario del movimiento de tu producto que ninguna búsqueda en el código te va a dar.
- Inventariar todas las animaciones vivas de un documento con sus objetivos y su timing resuelto.
- Congelar el documento en un instante determinista para capturas y pruebas visuales.
- Identificar qué animación está afectando a un elemento concreto y de dónde viene.
- Detectar animaciones que nunca terminan, que no se cancelan o que se solapan.
El inventario
document.getAnimations() devuelve todas las animaciones activas del documento: transiciones CSS, animaciones CSS y animaciones creadas con WAAPI, incluidas las que crea una librería que delegue en element.animate(). Cada una es un objeto Animation, y las de CSS son subclases con información extra: CSSAnimation lleva animationName y CSSTransition lleva transitionProperty.
function inventario() {
return document.getAnimations().map((a) => {
const t = a.effect?.getComputedTiming?.() ?? {};
return {
tipo: a.constructor.name,
nombre: a.animationName ?? a.transitionProperty ?? a.id ?? "sin nombre",
objetivo: a.effect?.target,
selector: describir(a.effect?.target),
estado: a.playState,
retardo: t.delay,
duracion: t.duration,
iteraciones: t.iterations,
total: t.activeDuration,
progreso: t.progress,
};
});
}
function describir(el) {
if (!el) return "-";
const clases = [...el.classList].slice(0, 2).map((c) => "." + c).join("");
return `${el.tagName.toLowerCase()}${el.id ? "#" + el.id : ""}${clases}`;
}
console.table(inventario());
Ese console.table en mitad de una interacción es probablemente la herramienta de diagnóstico con mejor relación entre esfuerzo y resultado de todo el nivel. Lo que suele revelar:
- Animaciones que no sabías que existían, casi siempre de un
transition: allen un componente compartido. - Transiciones sobre propiedades absurdas:
visibility,z-index,border-coloren elementos sin borde. - Animaciones con
iterations: Infinityque llevan corriendo desde que cargó la página. - Duraciones resueltas que no coinciden con lo que creías, porque una abreviatura interpretó los tiempos al revés.
- El mismo elemento con tres animaciones simultáneas sobre la misma propiedad.
getComputedTiming() es la parte valiosa: devuelve el timing resuelto, con duration en milisegundos aunque lo escribieras en segundos, activeDuration incluyendo todas las iteraciones, y progress como fracción entre 0 y 1 del ciclo actual. Es la verdad, no lo que escribiste.
Para un elemento concreto, element.getAnimations({ subtree: true }) incluye sus descendientes y sus pseudoelementos, que es como se descubre que quien se mueve es un ::before que nadie recordaba.
Congelar el documento
Para una captura de pantalla, una prueba visual o simplemente para medir un espaciado a mitad de una transición, hace falta un estado determinista. Estas tres funciones lo consiguen.
// Pausa todo donde esté.
const pausarTodo = () => document.getAnimations().forEach((a) => a.pause());
// Lleva todo a un instante concreto del ciclo. 0 es el principio, 1 el final.
function congelarEn(fraccion = 1) {
for (const a of document.getAnimations()) {
const t = a.effect.getComputedTiming();
const finitas = Number.isFinite(t.activeDuration);
a.currentTime = finitas ? t.activeDuration * fraccion : t.duration * fraccion;
a.pause();
}
}
// Termina lo finito y cancela lo infinito: el estado de reposo real.
function estabilizar() {
for (const a of document.getAnimations()) {
const t = a.effect.getComputedTiming();
if (Number.isFinite(t.activeDuration)) a.finish();
else a.cancel();
}
}
estabilizar es la que quieres antes de cualquier comparación visual automatizada, y es esencialmente lo que hace Playwright con la opción animations: "disabled" de sus capturas. Si usas otra herramienta, esta función te da el mismo comportamiento:
await page.evaluate(() => {
for (const a of document.getAnimations()) {
const t = a.effect.getComputedTiming();
Number.isFinite(t.activeDuration) ? a.finish() : a.cancel();
}
});
await page.screenshot({ path: "estado.png" });
Sin esto, las pruebas visuales de una interfaz con animaciones fallan de forma intermitente y nadie sabe por qué: la captura se toma en un punto arbitrario de una transición y cada ejecución cae en un punto distinto.
Y congelarEn(0.5) tiene un uso que nadie espera: auditar el estado intermedio. Muchas animaciones son correctas al principio y al final y producen un estado intermedio ilegible —texto sobre fondo casi del mismo color, elementos superpuestos, contraste insuficiente— que dura ciento cincuenta milisegundos y nadie ha mirado nunca. Congela a la mitad y revisa el contraste.
Quién anima esto
La pregunta que más se hace y peor se responde buscando en el código. Aquí se responde en el navegador:
// Selecciona el elemento en el panel de elementos y luego, en la consola:
$0.getAnimations({ subtree: true }).map((a) => ({
nombre: a.animationName ?? a.transitionProperty ?? a.id,
propiedades: [...new Set(a.effect.getKeyframes().flatMap((k) =>
Object.keys(k).filter((p) => !["offset", "computedOffset", "easing", "composite"].includes(p))
))],
duracion: a.effect.getComputedTiming().duration,
estado: a.playState,
}));
getKeyframes() devuelve los keyframes computados: incluye el offset resuelto de cada uno, el easing por keyframe, y los valores tal como el navegador los interpretó. Para una transición CSS ves los dos extremos con los valores reales, que es la única forma fiable de saber desde qué valor está partiendo algo cuando el resultado no es el esperado.
Dos límites a conocer: no atraviesa los iframe —cada documento tiene su propia lista— y una animación que ya terminó y no está retenida desaparece de la lista, así que para inspeccionar el final hace falta fill: "forwards" o llamar a persist().
Detectar problemas
Cuatro comprobaciones que se pueden dejar puestas en desarrollo y que detectan fallos reales.
Animaciones infinitas que nadie ve. Un bucle corriendo fuera de la pantalla o detrás de un modal es coste puro.
setInterval(() => {
const eternas = document.getAnimations().filter((a) => {
const t = a.effect?.getComputedTiming?.();
return t && !Number.isFinite(t.activeDuration) && a.playState === "running";
});
if (eternas.length) console.warn(`${eternas.length} animaciones infinitas activas`, eternas);
}, 5000);
Animaciones sobre elementos desconectados. Un nodo que se quitó del DOM con una animación todavía viva es una fuga: el objeto retiene el elemento.
const fugas = document.getAnimations()
.filter((a) => a.effect?.target && !a.effect.target.isConnected);
console.assert(fugas.length === 0, "animaciones sobre nodos desconectados", fugas);
Solapes sobre la misma propiedad. Dos animaciones escribiendo transform en el mismo elemento producen un resultado que depende del orden de composición, y casi nunca es lo que se quería.
El estado tras una interrupción. Dispara una animación, interrúmpela a mitad con otra acción y ejecuta el inventario. Si quedan animaciones en estado paused o running que deberían haber muerto, tienes una máquina de estados incompleta, y ese es el origen de la mayoría de los defectos de animación en producción.
Hay una diferencia epistemológica entre getAnimations() y leer tu código, y no es de comodidad. Tu código es la intención: lo que quisiste que pasara, expresado en varias hojas de estilo, varios componentes y a veces varias librerías, escritas en momentos distintos por personas distintas. getAnimations() es el hecho: la lista de animaciones que el navegador tiene efectivamente en marcha ahora mismo, después de que la cascada haya resuelto conflictos, la especificidad haya descartado reglas, las abreviaturas se hayan expandido, los valores heredados se hayan sustituido y los eventos de la última media hora hayan creado, cancelado y sustituido objetos. Entre las dos cosas hay una distancia que crece con el tamaño del proyecto y con el número de manos, y esa distancia es exactamente donde viven los bugs de animación: no en la línea que escribiste mal, sino en la interacción entre tres líneas que cada una por separado está bien. Lo importante de tener acceso al hecho es que cambia el tipo de pregunta que se puede investigar. Con el código puedes preguntarte qué debería pasar; con el inventario puedes preguntarte qué está pasando y qué sobra, que es una pregunta de auditoría, no de lectura, y tiene respuesta objetiva. Los equipos que ejecutan el inventario de vez en cuando sobre sus pantallas principales encuentran siempre lo mismo, y siempre les sorprende: entre dos y cinco veces más animaciones de las que creían tener, la mayoría procedentes de un transition: all puesto hace años en un componente base, sobre propiedades que nadie quería animar, costando trabajo en cada interacción de la aplicación. Ese hallazgo no se produce leyendo código, porque el código donde está el problema es una línea perfectamente razonable en un fichero que nadie tiene motivo para abrir. Se produce preguntándole al navegador.