wandres.dev
MOTION Y LAS ALTERNATIVAS · El resto del ecosistema

Muelles, equipos y el árbol de decisión honesto

Las librerías de física del movimiento y qué aportan sobre una curva, qué elige de verdad cada tipo de equipo y por qué, y el árbol completo de cuándo basta CSS, cuándo WAAPI, cuándo una declarativa y cuándo un motor.

⏱ 20 min

Después de siete niveles de GSAP es fácil llegar aquí convencido de que la respuesta correcta es siempre la más potente. No lo es. La mayoría de las interfaces del mundo se mueven bien con CSS y una librería de cuarenta líneas, y la mayoría de los equipos que eligieron el motor completo lo hicieron para tres animaciones y ahora lo cargan en todas las páginas. Esta lección pone cada opción en su sitio con el mejor argumento a favor de cada una.

🎯 Al terminar esta lección sabrás
  • Explicar qué aporta un muelle sobre una curva de Bézier y en qué casos concretos no aporta nada.
  • Comparar react-spring con los muelles integrados en Motion en términos de modelo, no de sintaxis.
  • Predecir qué elige cada perfil de equipo y por qué esa elección suele ser correcta para ellos.
  • Recorrer el árbol de decisión completo desde CSS puro hasta el motor completo.

Los muelles y qué aportan de verdad

Una curva de Bézier describe una trayectoria fija en un tiempo fijo. Un muelle no describe una trayectoria: describe un sistema con una posición, una velocidad y unas fuerzas, y la trayectoria es lo que sale de integrarlo. La diferencia práctica se reduce a una sola cosa, y todo lo demás son consecuencias: un muelle tiene estado, y una curva no.

De ahí salen las tres ventajas reales.

La interrupción es correcta sin que hagas nada. Si un elemento va hacia la derecha a doscientos píxeles por segundo y le cambias el destino, el muelle sigue moviéndose a doscientos píxeles por segundo y curva hacia el nuevo objetivo. Con una curva tienes que decidir tú qué hacer: reiniciar desde el valor actual con la misma duración produce un frenazo visible, y calcular la velocidad para compensar es reimplementar un muelle peor.

El movimiento continuo no tiene principio ni fin. Un elemento que sigue al puntero con un muelle no tiene animaciones que empiecen y terminen: tiene un objetivo que se actualiza y un sistema que persigue. Modelar eso con curvas exige lanzar una animación nueva por evento y cancelar la anterior.

Los parámetros son físicos y componen. Dos muelles con la misma rigidez y distinta masa se sienten como dos objetos del mismo material con distinto peso. Dos curvas con distinta duración solo se sienten como dos duraciones.

Y la desventaja, que se menciona poco: un muelle no tiene duración. No puedes decir “esto dura trescientos veinte milisegundos”, porque la duración es una consecuencia de la rigidez, la amortiguación, la masa y la distancia. Si tu sistema de diseño está construido sobre tokens de duración, los muelles no encajan en él sin traducción. Las parametrizaciones modernas —bounce más duration en Motion— existen precisamente para tapar ese hueco: por dentro siguen siendo un muelle, pero por fuera te dejan hablar en el idioma del diseño.

En React, el proyecto de referencia sigue siendo @react-spring/web. Su modelo es distinto del de Motion en un punto que importa: react-spring está construido alrededor del muelle, no lo añade como un tipo de transición más.

import { useSpring, useTransition, animated } from "@react-spring/web";

function Panel({ abierto }) {
  const estilos = useSpring({
    x: abierto ? 0 : -320,
    opacity: abierto ? 1 : 0,
    config: { tension: 260, friction: 30 },
  });
  return <animated.aside style={estilos}>...</animated.aside>;
}

tension es la rigidez y friction la amortiguación; hay presets con nombre (gentle, wobbly, stiff, slow) para no tener que sintonizar a mano. useTransition cubre el equivalente de la presencia: entradas, salidas y actualizaciones de una lista con muelles por elemento.

Cuándo react-spring sigue siendo la mejor elección. Cuando el movimiento del producto es continuo y gobernado por gestos —arrastrar hojas, deslizar tarjetas, seguir el puntero— y quieres que la física sea el modelo mental de todo el equipo, no una opción. Su ecosistema con @use-gesture/react está pensado para eso y encaja mejor que ninguna alternativa. Cuándo no. Cuando la mayor parte de tu movimiento son entradas, salidas y cambios de layout con duraciones definidas por diseño; ahí estás pagando un modelo que no usas, y Motion te da muelles cuando los necesitas sin obligarte a pensar en muelles siempre.

Qué elige cada equipo

No es una cuestión de gusto. Cada perfil tiene restricciones distintas y la elección correcta cambia con ellas.

El estudio creativo. Tres personas, proyectos de tres meses, el movimiento es el producto. Necesitan techo alto, secuencias largas, scroll con pin, texto dividido, SVG morfeado. Eligen un motor completo y tienen razón: el coste de mantenimiento a largo plazo no existe porque no hay largo plazo, y el coste en kilobytes lo compensa el hecho de que la página entera es la animación.

El equipo de producto SaaS. Quince personas, la aplicación vive años, el movimiento es funcional: paneles, modales, listas, cambios de layout. Nadie tiene “animación” en su descripción de puesto. Eligen la librería declarativa del framework y tienen razón: lo que les mata no es el techo, es que dentro de dos años alguien que no escribió el código tenga que tocarlo sin romper la salida del modal.

El equipo de plataforma o de sistema de diseño. Publican componentes que consume otra gente. Su restricción dominante es no imponer dependencias a los consumidores. Eligen CSS y WAAPI, y tienen razón: una librería de animación dentro de un componente de sistema de diseño es una dependencia transitiva que aparece en el bundle de todo el mundo, con su versión, sus conflictos y su ciclo de actualización.

El sitio de contenido. Documentación, blog, comercio. La restricción es el rendimiento en dispositivos malos y el presupuesto de bytes en la ruta crítica. Eligen CSS puro más auto-animate o motion/mini cargado bajo demanda, y tienen razón: cada kilobyte compite con el contenido, que es el producto.

El equipo con una aplicación gestual. Editores, lienzos, herramientas de manipulación directa. Todo su movimiento responde a un dedo o a un puntero y tiene que poder interrumpirse en cualquier instante. Eligen muelles como modelo por defecto, y tienen razón: es el único modelo en el que la interrupción es correcta por construcción.

El patrón que se repite: la elección correcta la determina la restricción dominante, y la restricción dominante casi nunca es la potencia. Es el mantenimiento, el peso, la independencia o el modelo de interacción. La potencia solo domina cuando el movimiento es el producto.

El árbol

flowchart TB
ini[Necesito movimiento] --> q0{Solo se reordena una lista}
q0 -->|Si| aa[auto animate]
q0 -->|No| q1{Lo dispara una clase o una pseudoclase}
q1 -->|Si| css[CSS puro]
q1 -->|No| q2{Es una secuencia con solapes o scroll con pin}
q2 -->|Si| motor[Motor completo]
q2 -->|No| q3{El nodo se monta y se desmonta con el estado}
q3 -->|Si| dec[Libreria declarativa del framework]
q3 -->|No| q4{Hacen falta muelles o interrupcion con velocidad}
q4 -->|Si| hib[Libreria hibrida imperativa]
q4 -->|No| waapi[WAAPI a pelo]
style ini fill:#f9e2af,color:#11111b
style css fill:#a6e3a1,color:#11111b
style aa fill:#a6e3a1,color:#11111b
style waapi fill:#89b4fa,color:#11111b
style dec fill:#cba6f7,color:#11111b
style hib fill:#94e2d5,color:#11111b
style motor fill:#fab387,color:#11111b

Cada rama merece una frase de justificación.

CSS puro cubre más de lo que la gente cree. Con @starting-style y transition-behavior: allow-discrete puedes hacer entrada y salida completas de un dialog o un popover sin una línea de JavaScript, y con interpolate-size: allow-keywords puedes animar a auto. Si tu movimiento es “cuando este elemento tiene esta clase se ve así”, no necesitas nada más, y lo que ganas es que funciona antes de que hidrate nada.

WAAPI a pelo es el escalón que casi todo el mundo se salta. element.animate() te da control programático completo —pausar, invertir, playbackRate, finished como promesa, getAnimations() para gobernar el documento entero— con cero dependencias. Su carencia real es la composición temporal: no hay timelines. Si tu necesidad es control y no secuenciación, aquí acabas.

auto-animate es un caso especial porque su coste es cero y su alcance es minúsculo. Si el problema es exactamente el suyo, no hay razón para escalar. Está detallado en Anime.js y auto-animate.

Librería declarativa cuando el movimiento es función del estado y hay montaje y desmontaje de por medio. El detalle de por qué esto no se puede resolver bien desde fuera del framework está en Motion en React.

Librería híbrida imperativa cuando quieres muelles, interrupciones correctas y una API compacta sin acoplarte a nada. Es el hueco de la animate vanilla de Motion y de Anime.js v4, tratado en Motion sin framework.

Motor completo cuando necesitas al menos una de estas cuatro: secuencias largas con solapes que se recalculan solos, scroll con pin y snap, división de texto con reflow responsive, o morfismo entre formas SVG arbitrarias. Si no necesitas ninguna, no lo necesitas.

⚠️
El árbol se recorre por animación, no por proyecto

Nada obliga a elegir una sola herramienta. Un proyecto sano tiene CSS para el noventa por ciento del movimiento, WAAPI para lo que necesita control, y una librería cargada bajo demanda en las dos pantallas que la justifican. Lo que hay que evitar es lo contrario: cargar un motor completo en todas las páginas porque una tiene una secuencia.

Lo que hace que la decisión envejezca bien

Independientemente de la rama que tomes, tres prácticas hacen que cambiar de opinión dentro de dos años sea barato.

No esparzas las duraciones y las curvas por el código. Si viven en variables CSS o en un módulo de tokens, migrar de librería es cambiar las llamadas; si viven como números literales en cuarenta ficheros, es reescribir el movimiento. Esto se trata a fondo en el nivel de coreografía y es, con diferencia, lo que más rentabilidad tiene.

Aísla la librería detrás de tus propias funciones. Un módulo con aparecer, desaparecer, desplazar es una superficie de cinco funciones; llamar a la librería directamente desde cada componente es una superficie de doscientas llamadas. La primera se reescribe en una tarde.

Carga la librería donde se usa. Un import() dinámico convierte la elección en reversible: si mañana la cambias, cambias un módulo, no el bundle de toda la aplicación.

La pregunta correcta no es cuál es mejor, es cuál es el coste de equivocarse

Todas las comparativas de librerías de animación optimizan la variable equivocada. Preguntan cuál es más potente, cuál pesa menos, cuál tiene mejor API, y son preguntas razonables cuyas respuestas no predicen casi nada, porque la mayoría de los proyectos no fracasan por haber elegido la librería subóptima: fracasan por haber elegido de forma irreversible. La pregunta que sí discrimina es cuánto cuesta cambiar de opinión, y la respuesta depende mucho más de cómo integres la librería que de cuál elijas. Una decisión mala con las duraciones en tokens, las llamadas detrás de una capa fina y la carga bajo demanda se revierte en dos días. Una decisión buena con la librería importada en ciento veinte componentes, los números literales repartidos y el paquete en el bundle inicial se convierte en un activo inamovible que nadie va a tocar aunque todos sepan que ya no encaja. Y esto tiene una implicación incómoda para quien disfruta de estas discusiones: el tiempo que dedicas a comparar tiene rendimientos decrecientes muy rápidos, y el tiempo que dedicas a aislar tiene rendimientos crecientes. Dos horas de comparativa te llevan del noventa por ciento de la información al noventa y cinco; dos horas construyendo una capa de cinco funciones te ahorran las tres semanas de la migración que vas a hacer dentro de dos años, cuando la restricción dominante de tu equipo ya no sea la que era. Que es lo que pasa siempre, porque los equipos cambian de tamaño, los productos cambian de fase y las librerías cambian de mantenedor. El objetivo no es acertar: es que acertar deje de ser importante.