El flujo completo de ajuste fino de una transición
El procedimiento de siete pasos que lleva de una animación que no convence a una que funciona, con las comprobaciones de rendimiento y de accesibilidad incluidas.
Las cuatro lecciones anteriores son piezas: capturar, leer la coreografía, ralentizar y editar. Esta las ordena en un procedimiento repetible que lleva de “esta transición no acaba de funcionar” a una transición terminada, incluyendo las dos comprobaciones que casi siempre se saltan y que son las que deciden si la animación es aceptable en el dispositivo de un usuario real y para una persona que ha pedido no ver movimiento.
- Aplicar los siete pasos del ajuste en orden sobre una transición concreta.
- Diagnosticar en qué paso está el problema cuando algo no convence.
- Comprobar el coste de una animación en el perfil antes de darla por buena.
- Verificar el comportamiento con movimiento reducido.
Los siete pasos
Paso 1: capturar y observar a velocidad normal. Provoca la transición con el panel abierto y míralas dos o tres veces sin analizar nada. Anota la impresión con una palabra: brusca, lenta, mecánica, confusa. Esa palabra orienta todo lo demás y se pierde en cuanto empiezas a mirar detalles.
Paso 2: leer la coreografía. Con el grupo seleccionado, mirar la línea de tiempo entera antes que ninguna animación individual. Buscar los cuatro defectos de coordinación: todo a la vez, hueco muerto, desbordamiento, orden invertido. En la mayoría de los casos el problema está aquí, y arreglarlo hace innecesarios los pasos siguientes.
Paso 3: ralentizar al décimo y buscar defectos. Saltos al empezar o al terminar, parpadeos de texto, desajustes entre elementos. Cada uno tiene una causa concreta y un arreglo concreto.
Paso 4: ajustar la curva a un cuarto de velocidad. Con el editor, hasta que el movimiento corresponda a lo que la interacción significa.
Paso 5: ajustar la duración a velocidad normal. Este paso obligatoriamente a velocidad real, y con el criterio de que la duración se justifica por lo que comunica, no por lo bonita que quedó.
Paso 6: comprobar el coste. Un perfil corto durante la animación. Es el paso que casi nadie hace.
Paso 7: comprobar con movimiento reducido. La emulación del panel de renderizado. También casi nunca se hace.
Si tienes que saltarte pasos por falta de tiempo, los que hay que conservar son el 2, el 5 y el 7. La coordinación es donde están la mayoría de los defectos, la duración es lo que el usuario percibe como velocidad de la aplicación, y el movimiento reducido es una cuestión de accesibilidad y no de acabado.
Paso 6: comprobar el coste
Una animación puede verse perfecta en tu equipo y arruinar la interfaz en un móvil. La comprobación son dos minutos.
Graba un perfil corto que contenga solo la transición, con la ralentización de CPU puesta al factor de tu dispositivo objetivo. Después, tres lecturas:
La pista de fotogramas. Si hay fotogramas largos o huecos durante la animación, hay un problema. Con la ralentización activada, ese problema existe de verdad en el dispositivo del usuario.
El reparto de trabajo. Una animación sana produce trabajo en las pistas del compositor y de la GPU, y muy poco en el hilo principal. Si el hilo principal está ocupado durante la animación, algo está mal: o la animación afecta a propiedades que fuerzan disposición y pintado, o hay otro trabajo compitiendo.
Los bloques de disposición. Si aparece disposición en cada fotograma, la animación está sobre una propiedad geométrica y hay que reformularla.
// Comprueba el coste de una animacion midiendo la cadencia durante su ejecucion
async function medirCosteDeAnimacion(provocar, duracionMs = 1000) {
const duraciones = [];
let anterior = performance.now();
let midiendo = true;
const paso = ahora => {
if (!midiendo) return;
duraciones.push(ahora - anterior);
anterior = ahora;
requestAnimationFrame(paso);
};
requestAnimationFrame(paso);
await provocar();
await new Promise(r => setTimeout(r, duracionMs));
midiendo = false;
const ordenadas = duraciones.slice().sort((a, b) => a - b);
const p = q => ordenadas[Math.floor(ordenadas.length * q)] ?? 0;
const perdidos = duraciones.filter(d => d > 32).length;
console.table([{
fotogramas: duraciones.length,
medianaMs: +p(0.5).toFixed(1),
p95Ms: +p(0.95).toFixed(1),
peorMs: +(ordenadas.at(-1) ?? 0).toFixed(1),
fotogramasLargos: perdidos
}]);
console.log(perdidos === 0
? 'Sin fotogramas perdidos. Repite con la ralentizacion de CPU activada.'
: 'Hay ' + perdidos + ' fotogramas largos: mira el perfil durante la animacion.');
return duraciones;
}
// Ejemplo ejecutable
const caja = document.createElement('div');
caja.style.cssText = 'width:60px;height:60px;background:#f9e2af;border-radius:10px';
document.body.append(caja);
await medirCosteDeAnimacion(async () => {
caja.animate(
[{ transform: 'translateX(0) scale(1)' }, { transform: 'translateX(300px) scale(1.3)' }],
{ duration: 800, easing: 'cubic-bezier(.16,1,.3,1)', fill: 'both' }
);
}, 900);
El número que decide es el de fotogramas largos. Cero con la ralentización de CPU puesta al factor del dispositivo objetivo es el criterio de aprobación; cualquier otra cosa es una animación que va a ir a tirones para una parte de tus usuarios.
Paso 7: movimiento reducido
Con la emulación activada en el panel de renderizado, vuelve a provocar la transición y comprueba tres cosas.
Que el movimiento se ha reducido de verdad, no solo la duración.
Que la interfaz sigue siendo comprensible. Si la animación era lo único que comunicaba de dónde venía un elemento, al quitarla puede quedar un cambio brusco desconcertante. La sustitución correcta suele ser una transición de opacidad corta, que informa del cambio sin producir desplazamiento.
Que nada se rompe. Un error muy común es que el código dependa del evento de finalización de la animación para hacer algo, y al desactivar la animación ese evento no llega nunca, dejando la interfaz a medias.
// Animar respetando la preferencia, sin perder la señal de finalizacion
function animarAccesible(el, keyframesCompletos, opciones = {}) {
const reducido = matchMedia('(prefers-reduced-motion: reduce)').matches;
const keyframes = reducido
? [{ opacity: 0 }, { opacity: 1 }] // solo aparicion, sin desplazamiento
: keyframesCompletos;
const config = {
duration: reducido ? 100 : (opciones.duration ?? 300),
easing: opciones.easing ?? 'cubic-bezier(.16,1,.3,1)',
fill: opciones.fill ?? 'both'
};
// Siempre devuelve una animacion real, asi que finished siempre resuelve
return el.animate(keyframes, config);
}
// Ejemplo ejecutable: la promesa de finalizacion funciona en los dos modos
const tarjeta = document.createElement('div');
tarjeta.textContent = 'Contenido';
tarjeta.style.cssText = 'padding:16px;background:#cba6f7;color:#11111b;border-radius:10px;width:180px';
document.body.append(tarjeta);
const anim = animarAccesible(tarjeta, [
{ opacity: 0, transform: 'translateY(20px) scale(.96)' },
{ opacity: 1, transform: 'none' }
]);
await anim.finished;
console.log('La animacion termino. Esto se ejecuta en los dos modos.');
La decisión de diseño que hace correcto ese código es devolver siempre una animación real, aunque sea trivial, en lugar de devolver nulo o saltarse la llamada cuando la preferencia está activa. Así el código que espera la finalización funciona igual en los dos modos, y esa es exactamente la clase de detalle que evita que la accesibilidad rompa funcionalidad.
Diagnóstico por síntoma
| La transición se percibe como… | Mira en el paso |
|---|---|
| Brusca al empezar o al terminar | 3, saltos inicial o final |
| Mecánica, sin vida | 4, curva lineal o inadecuada |
| Lenta, aunque se vea bien | 5, duración excesiva |
| Desordenada, confusa | 2, orden y solapamiento |
| Que tartamudea | 6, coste, y el perfil |
| Que no comunica nada | 2, la coreografía no cuenta la relación |
| Que va bien aquí y mal en móvil | 6, con ralentización de CPU |
Merece la pena señalar por qué el movimiento acaba siendo, en casi todos los productos, la parte peor cuidada de la interfaz a pesar de que todo el mundo sabe que importa. La razón no es técnica ni de criterio: es que el movimiento no cabe en ninguno de los artefactos con los que un equipo revisa su trabajo. Un diseño se revisa en una imagen estática. Una propuesta se presenta en un documento. Un cambio se revisa en un diff de código, donde una curva es cuatro números sin significado visible. Una prueba automatizada comprueba estados finales, no trayectorias. En todo ese circuito, la única persona que llega a ver el movimiento es quien lo implementa, y solo mientras lo implementa. El resultado previsible es que las animaciones no se revisan: se aceptan. Y como no se revisan, no mejoran, y como no mejoran, se acumulan duraciones arbitrarias y curvas por defecto hasta que el producto entero se percibe como poco cuidado sin que nadie sepa señalar dónde. Hay dos prácticas concretas que corrigen esto y ninguna de las dos es cara. La primera es hacer el movimiento revisable: adjuntar una grabación corta a cada cambio que introduzca o modifique una animación, y adjuntarla también ralentizada. Cuesta un minuto por cambio y convierte una parte invisible del trabajo en algo sobre lo que se puede opinar con criterio. La segunda es centralizar las decisiones en un conjunto pequeño de curvas y duraciones con nombre, de modo que la mayoría de las animaciones no requieran ninguna decisión: se elige entre siete curvas y cuatro duraciones, todas justificadas una vez y reutilizadas siempre. Eso reduce el espacio de error, hace que el producto se sienta coherente, y libera el juicio detallado para las pocas transiciones que de verdad lo merecen. Y hay un beneficio secundario nada menor: cuando las duraciones son constantes con nombre, respetar la preferencia de movimiento reducido es un cambio en un sitio y no una cacería por trescientos ficheros.