Communitygithub.com

Rendimiento de Animaciones

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

¿Qué es Rendimiento de Animaciones?

Esta habilidad de diagnóstico está diseñada para identificar y solucionar problemas de rendimiento en animaciones web, asegurando que cada fotograma se renderice a tiempo. Se enfoca en dos áreas principales: qué propiedades animar (priorizando `transform` y `opacity` para evitar costosos cálculos de layout y pintura) y dónde se ejecuta la animación (en el hilo principal o acelerada por hardware). La habilidad cubre el pipeline de layout/paint/composite, el intercambio de propiedades más económicas, la comparación entre CSS, WAAPI y `requestAnimationFrame`, el camino acelerado por hardware de Motion, el re-renderizado de React por fotograma, el recálculo de variables CSS heredadas, el costo del desenfoque y cuándo `will-change` realmente ayuda. Es útil para cualquier desarrollador que enfrente animaciones entrecortadas, con tirones o que pierden fotogramas, y proporciona un flujo de trabajo estructurado de tres fases: identificar las animaciones, diagnosticar la causa raíz y reportar los hallazgos con soluciones concretas.

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

Preguntar en tu IA favorita

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

Documentación

¿Qué hace Rendimiento de Animaciones?

Respuesta Inicial

Cuando se invoque esta habilidad 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: asegurar que el movimiento llegue a cada fotograma. No elige la easing ni la 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 Importa

Imagina Sonner funcionando a 30fps. Misma easing, mismas duraciones, mismo código. A nadie le habría importado. El rendimiento no es un pulido que se aplica después de que el movimiento está bien; es lo que permite que un 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 que se describen a continuación en producción. Sabes que el MacBook del desarrollador es el dispositivo menos representativo en el que una animación se ejecutará jamás.

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 120Hz el presupuesto es la mitad de lo que piensas.
  2. Poner will-change y dar 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 ninguna de esas dos cosas.

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 costosa "parece estar bien".
  2. Nombra la causa antes de cambiar el código. Cada solución que propongas debe nombrar cuál de las causas siguientes aborda. "Añadí will-change, parece más fluido" no es un diagnóstico.
  3. Nunca concluyas "está bien" basándote en una máquina rápida. Indica qué hallazgos verificaste con el código y cuáles necesitan una grabación en un dispositivo real.
  4. Nunca uses transition: all. Enumera las propiedades exactas. all anima cualquier cosa que cambie después, 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 conceder. Las propiedades que animas es algo que tú controlas. Dedica esfuerzo a lo segundo.

La Tabla de Síntomas

Empieza aquí. Empareja lo que el usuario está viendo, 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
x/y de Motion pierde fotogramasActualiza a motion 14.2+, o anima la cadena completa de transform
Animación hecha a mano pierde fotogramas en ReactEscribe en ref.current.style, no en el estado
El drag 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 en Safari)Mantén el blur() animado por debajo de 20px
Propiedades inesperadas se animanReemplaza transition: all con propiedades explícitas
El elemento se desplaza 1px cuando el movimiento comienza o terminawill-change: transform, solo cuando lo veas

El Presupuesto de Fotogramas

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

Para ponerlo en contexto: un recálculo de layout en una página compleja puede tomar más de 100ms. Eso no es un fotograma perdido, sino una docena.

Dos cosas determinan 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 la página está haciendo.

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 la pantalla.

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

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

transform y opacity cambian cómo se ve un elemento, no dónde se sitúa en el flujo del documento, por lo que nada necesita re-medición. Cualquier otra cosa en el nivel de composite necesita pruebas entre navegadores y dispositivos; el soporte es desigual.

Bajando de nivel:

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)
Cambios 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 cosas se derivan de esto:

  • 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 de scale() se ven igual, y solo una de ellas no puede degradarse en un dispositivo más lento. Elige la que no tiene desventajas.
  • 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. Para un panel junto a una barra lateral que se abre, eso es la página entera re-layoutizada 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, animaciones de layout de Motion)Hilo principal, siempre
Web Animations API (element.animate(), animate() de Motion y componentes motion/react)Puede acelerarse por hardware
Transiciones y keyframes CSSPuede acelerarse 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 entrega el control a WAAPI puede acelerarse, un bucle rAF no.

Acelerado de forma fiable: transform y opacity. Ganando soporte: 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ña durante la navegación, cualquier cosa que se active junto con la carga 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 perdió fotogramas. Moverlo a una animación CSS lo solucionó.

Motion entrega opacity, transform, clip-path y filter a WAAPI, por lo que pueden acelerarse. Los atajos x, y, scale, rotate dependen de la versión:

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

Desde motion 14.2, los atajos en un elemento HTML se combinan en una animación transform acelerada, así que manténlos. En versiones anteriores, cambia a la forma de cadena cuando la animación tenga que sobrevivir a un hilo principal ocupado. En todas las versiones, estas permanecen en el hilo principal: animaciones de layout (layout, layoutId), onUpdate, transformTemplate, inercia, y valores impulsados cada fotograma por un motion value (drag, useTransform).

La aceleración es una mejora progresiva. Los navegadores la rechazan silenciosamente en 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 decisión correcta para elementos de tamaño desconocido (así es como Sonner y Vaul posicionan toasts y drawers), y la corrección es más importante que 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 cada fotograma: el componente se re-renderiza cada 16.7ms y solo el renderizado ya consume el presupuesto.

// Pierde fotogramas: un re-renderizado por fotograma
setY(nextY);

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

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

Dos Trampas Que Parecen Correctas

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

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

// Recalcula 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 cambiar una en un padre desencadena un recálculo de estilo para cada hijo. Cuanto más contenido, más lento es cada fotograma. Sospecha esto siempre que una animación transform se degrade a medida que el contenido crece.

Desenfoque. filter: blur() se vuelve costoso rápidamente, especialmente en Safari. Mantén el desenfoque animado por debajo de 20px; 2–5px es suficiente para suavizar un crossfade.

will-change

Un navegador puede pasar una animación de la CPU a la GPU en medio del vuelo, y esa transferencia 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 ninguna transferencia:

.drawer {
  will-change: transform;
}

Añádelo solo una vez que veas el desplazamiento o la pérdida de fotogramas medida. Los navegadores ya promueven 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 contenedores grandes.

Donde realmente vale la pena es en una pintura costosa, como un elemento grande desenfocado o con 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 JS?

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

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

La división de trabajo: CSS para movimiento simple y cualquier cosa que se reproduzca mientras la página está ocupada; Framer Motion para movimiento complejo, interrumpible e impulsado por gestos.

Flujo de Trabajo

Fase 1: Encuentra qué anima

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

ElementoControladorPropiedadesSe reproduce durante
Indicador de pestañamotion/react layoutIdlayoutcambio de ruta
DrawerCSS transitiontransformarrastre del usuario
Hover de tarjetaCSS transitionallhover

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

Fase 2: Diagnostica

Trabaja hacia abajo en 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 layout y son fáciles de pasar por alto.
  2. ¿La falta de fluidez coincide con la página ocupada? Navegación, fetching, 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 rAF, pointer, o manejadores de scroll.
  4. ¿Empeora a medida que el contenido crece? Una variable CSS heredada, o demasiados nodos animando a la vez.
  5. ¿Se está animando una variable CSS en absoluto? ¿O los atajos x/y/scale de Motion en una versión anterior a la 14.2?
  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 anima.

Confirma con una grabación, no con sensaciones: el panel de Rendimiento de DevTools para fotogramas perdidos y tareas largas, el panel de Animaciones para ralentizar el movimiento. Perfila en un teléfono real de gama media 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: Informa

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 en dispositivo

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

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Skills relacionados