CommunityProgramación y desarrollogithub.com

Bront888/skills

Mantén las animaciones web dentro del presupuesto de fotogramas: anima solo propiedades de composición, mantén la animación fuera del hilo principal cuando la página esté ocupada y encuentra la causa cuando una animación pierde fotogramas.

¿Qué es skills?

Esta habilidad (skill) es una herramienta de diagnóstico para desarrolladores que trabajan con animaciones web en JavaScript, CSS y librerías como Framer Motion. Su objetivo principal es garantizar que cada fotograma de una animación se renderice a tiempo, evitando tirones y pérdidas de fotogramas (jank). Está diseñada para ser usada en plataformas como Cursor, Codex o Claude Code, y su enfoque es puramente técnico: no se ocupa de elegir easing, duración o decidir si una animación debe existir, sino de optimizar el rendimiento. La habilidad se basa en la filosofía de ingeniería de diseño de Emil Kowalski y cubre en detalle el pipeline de layout/paint/composite, la elección de propiedades a animar (priorizando `transform` y `opacity`), la comparación entre CSS, WAAPI y `requestAnimationFrame`, el impacto de las re-renderizaciones de React, el coste de las variables CSS heredadas, el filtro `blur()`, y cuándo `will-change` es realmente beneficioso. Ofrece una tabla de síntomas para un diagnóstico rápido, un flujo de trabajo estructurado en tres fases (encontrar, diagnosticar, reportar), y reglas estrictas como no usar `transition: all` y no atribuir la mejora a `will-change` sin antes identificar la causa raíz.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Bront888/skills/tree/HEAD/skills/animation-performance

Installed? Explore more Programación y desarrollo skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Rendimiento de Animaciones

Respuesta Inicial

Cuando esta habilidad se invoque por primera vez sin una pregunta específica, responde únicamente con:

Estoy listo para descubrir por qué tu animación pierde fotogramas y hacerla fluida; mi conocimiento proviene de la filosofía de ingeniería de diseño de Emil Kowalski.

No proporciones ninguna otra información hasta que el usuario haga una pregunta.

Una habilidad de diagnóstico. Hace UNA cosa: lograr que el movimiento llegue a cada fotograma. No elige easing ni duración (eso es animate), no juzga si el movimiento debería existir (eso es review-animations), ni maneja los hilos de React Native (eso es animate-expo). La pregunta aquí nunca es "¿se ve bien?" sino "¿cada fotograma termina a tiempo?".

Por Qué Esto Es Importante

Imagina Sonner ejecutándose a 30 fps. Mismo easing, mismas duraciones, mismo código. A nadie le habría importado. El rendimiento no es una capa de pulido que se aplica después de que el movimiento es bueno; es lo que permite que el buen movimiento se perciba en absoluto.

Postura Operativa

Eres un ingeniero de diseño que ha lanzado drawers, toasts y paneles a dispositivos reales y ha sufrido cada una de las trampas a continuación en producción. Sabes que el MacBook del desarrollador es el dispositivo menos representativo en el que se ejecutará la animación.

Dos modos de fallo, y el primero es peor:

  1. "Es fluido en mi máquina." Un portátil rápido oculta casi todos los problemas de esta habilidad. El estándar es un teléfono de gama media, y en una pantalla de 120 Hz el presupuesto es la mitad de lo que piensas.
  2. Espolvorear will-change y darlo por solucionado. Es el último paso, no el primero. La mayoría de los tirones son causados por la propiedad incorrecta o un hilo principal ocupado, y will-change no soluciona ni lo uno ni lo otro.

Reglas Estrictas

  1. transform y opacity por defecto. Cualquier otra cosa necesita una razón. Si una propiedad más barata produce el mismo resultado visual, úsala, incluso si la cara "parece estar bien".
  2. Nombra la causa antes de cambiar el código. Cada solución que propongas nombra cuál de las causas que aparecen a continuación aborda. "Añadí will-change, parece más fluido" no es un diagnóstico.
  3. Nunca concluyas "está bien" desde una máquina rápida. Di qué hallazgos verificaste desde el código y cuáles necesitan una grabación en un dispositivo real.
  4. Nunca transition: all. Enumera las propiedades exactas. all anima cualquier cosa que cambie a continuación, incluyendo propiedades de layout que nadie pretendía animar.
  5. No sacrifiques la corrección por una optimización de mejor esfuerzo. La aceleración por hardware es algo que el navegador puede otorgar. Las propiedades que animas son algo que tú controlas. Dedica esfuerzo a lo segundo.

La Tabla de Síntomas

Empieza aquí. Relaciona lo que el usuario está viendo y luego lee la sección correspondiente.

ProblemaSolución
La animación tiene tironesAnima transform/opacity, no width/height/top/margin
La animación de sombra es entrecortadafilter: drop-shadow()
Tirones solo durante la navegación o cargaMueve la animación a CSS o WAAPI para que se ejecute fuera del hilo principal
Framer Motion x/y pierde fotogramasAnima la cadena transform completa
La animación hecha a mano pierde fotogramas en ReactEscribe en ref.current.style, no en el estado
El arrastre se vuelve más lento a medida que la lista creceDeja de animar una variable CSS heredada; establece transform directamente
La transición de desenfoque se retrasa (especialmente Safari)Mantén el blur() animado por debajo de 20px
Se animan propiedades inesperadasReemplaza transition: all por propiedades explícitas
El elemento se desplaza 1px cuando el movimiento comienza o terminawill-change: transform, solo una vez que lo veas

El Presupuesto de Fotogramas

60 fps es lo que el cerebro lee como movimiento fluido y coincide con la mayoría de las frecuencias de actualización. Esto le da al navegador 16,7 ms (1000/60) por fotograma. En una pantalla de 120 Hz, ahora la mayoría de los teléfonos y muchos portátiles, son 8 ms. Diseña pensando en el número más pequeño.

Para que te hagas una idea: un recálculo de layout en una página compleja puede llevar más de 100 ms. Eso no es un fotograma perdido, sino una docena.

Dos cosas deciden si te mantienes dentro del presupuesto:

  1. Qué animas: cuánto trabajo cuesta cada fotograma.
  2. Dónde se ejecuta: si ese trabajo puede ser bloqueado por todo lo demás que está haciendo la página.

Qué Animas

Para mostrar un cambio visual, el navegador ejecuta hasta tres pasos:

  • Layout: calcula el tamaño y la posición de cada elemento.
  • Paint: dibuja la página en capas.
  • Composite: coloca esas capas en pantalla.

Cada propiedad entra en este pipeline en uno de tres puntos, y el costo difiere en un orden de magnitud:

NivelPropiedadesCosto
Solo composicióntransform, opacity, y en Chrome/Firefox actuales también filter, clip-path, background-colorMás barato. Los navegadores promueven estas a su propia capa.
Paint + composiciónbox-shadow, border-radius, colorSin necesidad de medir de nuevo, pero un redibujado cada fotograma.
Layout + paint + composiciónwidth, height, padding, margin, top, left, border-widthMás caro. Un elemento que crece puede mover todo lo que lo rodea.

transform y opacity cambian cómo se ve un elemento, no dónde se encuentra en el flujo del documento, por lo que nada necesita ser medido de nuevo. Cualquier otra cosa en el nivel de composición necesita pruebas en varios navegadores y dispositivos; el soporte es desigual.

Cambiando a un nivel inferior:

En lugar deAnima
width / height / padding para crecer o encogerscale()
top / left / margin para movertranslate() (los porcentajes son relativos al tamaño del propio elemento)
intercambios de display / visibilityopacity
box-shadowfilter: drop-shadow(...)

El último es el que la gente pasa por alto. Una sombra animada parece inofensiva, pero vuelve a pintar el elemento en cada fotograma.

Dos consecuencias:

  • Las animaciones de layout no siempre pierden fotogramas. Un elemento posicionado absolutamente, o uno con pocos hijos, puede animar padding sin costo visible. Pero una animación de padding y una animación de scale() se ven igual, y solo una de ellas no puede empeorar en un dispositivo más lento. Elige la que no tiene inconvenientes.
  • Las propiedades de layout también mueven las cosas equivocadas. Animar el margin-left de una barra lateral empuja el contenido a su lado en cada fotograma; translateX no lo hace. Para un panel junto a una barra lateral que se abre, eso es toda la página re-maquetada 60 veces por segundo.

Dónde Se Ejecuta

JavaScript siempre se ejecuta en el hilo principal. Una animación impulsada por requestAnimationFrame compite con todo lo demás que el navegador está haciendo. Cuando el hilo principal está ocupado, el código de la animación no puede ejecutarse y el fotograma llega tarde.

ControladorHilo
JS + requestAnimationFrame (GSAP, componentes motion/react)Hilo principal, siempre
Web Animations API (element.animate(), animate() de Motion)Puede tener aceleración por hardware
Transiciones CSS y keyframesPueden tener aceleración por hardware

La aceleración por hardware significa que la animación se ejecuta fuera del hilo principal, por lo que se mantiene fluida sin importar lo que JavaScript esté haciendo. "JS" y "hilo principal" no son sinónimos: una librería JS que delega en WAAPI puede acelerarse, un bucle rAF no.

Acelerado de forma fiable: transform y opacity. Con soporte creciente: filter, clip-path, background-color.

El patrón a vigilar es el movimiento que se ejecuta mientras la página está realizando trabajo pesado: transiciones de página, indicadores de pestañas durante la navegación, cualquier cosa que se active junto con la obtención de datos o la hidratación. El panel de Vercel animaba su indicador de pestaña activa con una animación de layout compartido de Framer Motion. El navegador estaba cargando la siguiente página exactamente en ese momento, por lo que perdía fotogramas. Moverlo a una animación CSS lo solucionó.

Framer Motion puede acelerarse, pero solo cuando la transformación es una cadena:

<motion.div animate={{ x: 100 }} />                          // hilo principal, puede tener tirones
<motion.div animate={{ transform: "translateX(100px)" }} />  // acelerado por hardware

Los atajos x, y, scale, rotate se implementan con variables CSS, y las variables CSS animadas no se aceleran. Los atajos se leen mejor, así que mantenlos por defecto y cambia a la forma de cadena cuando la animación tenga que sobrevivir a un hilo principal ocupado.

La aceleración es una mejora progresiva. Los navegadores la rechazan silenciosamente bajo condiciones que cambian entre versiones; Chrome, por ejemplo, históricamente no aceleraba las transformaciones basadas en porcentajes. Eso no es una razón para dejar de usar translateY(100%). Los porcentajes son la opción correcta para elementos de tamaño desconocido (así es como Sonner y Vaul posicionan toasts y drawers), y la corrección supera a una optimización de mejor esfuerzo.

React: No Animes a Través del Estado

Las librerías de animación animan fuera del ciclo de renderizado de React, por lo que están bien. La trampa es una animación hecha a mano que llama a setState en cada fotograma: el componente se re-renderiza cada 16,7 ms y solo el renderizado ya supera el presupuesto.

// Pierde fotogramas: una re-renderización por fotograma
setY(nextY);

// Fluido: sin re-renderización
ref.current.style.transform = `translateY(${nextY}px)`;
// o con un valor de motion: y.set(nextY)

Busca actualizaciones de estado dentro de requestAnimationFrame, manejadores de pointermove, scroll o arrastre.

Dos Trampas Que Parecen Correctas

Ambas usan transform y aún así pierden fotogramas.

Variables CSS heredadas. El arrastre de Vaul se volvía lento una vez que un drawer contenía más de ~20 elementos, sin que hubiera re-renderizaciones de por medio. La distancia de arrastre se escribía en una variable CSS que translateY() leía:

// Recalcula los estilos para cada descendiente, cada fotograma
const style = { "--swipe-amount": `${draggedDistance}px` };

// Solo toca este elemento
const style = { transform: `translateY(${draggedDistance}px)` };

Las variables CSS se heredan, por lo que cambiarlas en un padre desencadena el recálculo de estilos para cada hijo. Cuanto más contenido, más lento es cada fotograma. Sospecha de esto cada vez que una animación de transform se degrada a medida que el contenido crece. Es la misma razón por la que los atajos de Framer Motion no se aceleran.

Desenfoque. filter: blur() se vuelve caro rápidamente, especialmente en Safari. Mantén el desenfoque animado por debajo de 20 px; 2–5 px son suficientes para suavizar un fundido cruzado.

will-change

Un navegador puede pasar una animación de la CPU a la GPU durante el vuelo, y ese traspaso puede hacer que el elemento se desplace un píxel al principio o al final. will-change promueve el elemento de antemano para que no se produzca ningún traspaso:

.drawer {
  will-change: transform;
}

Añádelo solo una vez que veas el desplazamiento o los fotogramas perdidos medidos. Los navegadores ya promueven los elementos que animan transform u opacity, por lo que en el camino común no hace nada, y cada capa promovida cuesta memoria de la GPU. Ponlo en el elemento que anima, nunca en * o en contenedores grandes.

Donde realmente vale la pena es en una pintura costosa, como un elemento grande con desenfoque o filtro. Promuévelo y mantén la capa pequeña; una capa pequeña es más barata de componer que una de pantalla completa.

¿Deberías Evitar la Animación con JavaScript?

No. La animación en el hilo principal solo sufre cuando la página está ocupada durante la misma, lo cual es raro, y ahora sabes cómo detectarlo.

Framer Motion te ofrece cosas que CSS no puede: springs reales, animaciones de layout compartido, animaciones de salida para componentes desmontados, gestos basados en momento. La cuestión es si necesitas esas cosas, no si la animación con JS está permitida.

La división práctica: CSS para movimiento simple y cualquier cosa que se reproduzca mientras la página está ocupada; Framer Motion para movimiento complejo, interrumpible y basado en gestos.

Flujo de Trabajo

Fase 1: Encontrar lo que anima

Enumera cada animación en el alcance con su controlador y propiedades:

ElementoControladorPropiedadesSe reproduce durante
Indicador de pestañamotion/react layoutIdlayoutcambio de ruta
Drawertransición CSStransformarrastre del usuario
Efecto hover de tarjetatransición CSSallhover

Expande transition: all y los atajos de Motion a lo que realmente animan.

Fase 2: Diagnosticar

Recorre esta lista. Está ordenada por la frecuencia con la que cada una es la causa:

  1. ¿Qué propiedades se animan? Cualquier cosa que no sea transform/opacity es la primera sospechosa: primero las propiedades de layout, luego box-shadow y border-radius, que no parecen de layout y son fáciles de pasar por alto.
  2. ¿Coinciden los tirones con que la página esté ocupada? Navegación, obtención de datos, hidratación, una tarea larga. Mueve la animación a CSS o WAAPI.
  3. ¿React se está re-renderizando por fotograma? Actualizaciones de estado en manejadores de rAF, pointer o scroll.
  4. ¿Empeora a medida que el contenido crece? Una variable CSS heredada, o demasiados nodos animándose a la vez.
  5. ¿Se anima alguna variable CSS en absoluto? Directamente, o a través de x/y/scale de Motion.
  6. ¿Algún blur() animado por encima de 20px?
  7. ¿Todavía se desplaza o tiene tirones? Ahora añade will-change: transform al elemento que se anima.

Confirma con una grabación, no por sensación: el panel de Rendimiento de las DevTools para fotogramas perdidos y tareas largas, el panel de Animaciones para ralentizar el movimiento. Perfila en un teléfono de gama media real mediante depuración remota. La limitación de CPU en un escritorio modela una CPU lenta, no una GPU débil o un presupuesto de memoria pequeño.

Fase 3: Reportar

Para cada hallazgo:

**[Elemento]**: `archivo.tsx:42`
Causa: [cuál de las causas anteriores]
Antes: `transition: all 200ms` (anima `padding`, desencadena layout)
Después: `transition: transform 200ms` con `scale(0.97)`
Verificado: desde el código / necesita una grabación del dispositivo

Ordena los hallazgos por coste: layout antes que paint, hilo principal ocupado antes que inactivo. Luego para. Aplica las correcciones cuando se te pida.

Skills relacionados