Communitygithub.com

Animación Web 60 FPS

Usa esta habilidad cuando el usuario solicite "arreglar animación CSS entrecortada", "hacer animación 60fps", "detener layout thrashing", "animar width/height/top/left suavemente", "convertir animación a transform", "animar box-shadow con rendimiento", "animar height auto", "animación FLIP" o "por qué mi animación de scroll/hover es entrecortada".

¿Qué es Animación Web 60 FPS?

Esta habilidad, denominada 'Animación Web 60 FPS', está diseñada para eliminar la causa principal de animaciones web entrecortadas: animar propiedades que obligan al navegador a recalcular el layout (reflow) o repintar en cada fotograma. Al entender el pipeline de renderizado del navegador (layout → paint → composite), se enfoca en animar únicamente `transform` y `opacity`, ya que estas propiedades se ejecutan completamente en el compositor (a menudo la GPU), omitiendo layout y repintado. Es útil para desarrolladores que enfrentan animaciones stuttering, efectos hover/scroll pesados o animaciones de tamaño/posición/sombra. La habilidad cubre técnicas como el reemplazo de animaciones costosas por equivalentes baratos (por ejemplo, usar `transform: scale()` en lugar de `width/height`), la técnica FLIP para animar cambios de layout sin distorsión, y estrategias para animar `height: auto` sin JS, como grid `0fr→1fr` o `interpolate-size: allow-keywords`. También incluye pautas para evitar layout thrashing, usar `will-change` con moderación, y verificar que las animaciones sean solo de compositor mediante herramientas de desarrollo. Está optimizada para plataformas como Claude Code, Cursor y Codex, y se entrega como un archivo HTML autocontenido con un mecanismo de congelación para capturas de pantalla deterministas.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/yokyznt/claude-skills-web-definitiva/tree/HEAD/skills/60fps-animation

Preguntar en tu IA favorita

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

Documentación

Animación Web con Rendimiento

Elimina la causa #1 de animaciones web entrecortadas: animar propiedades que fuerzan al navegador a recalcular el layout (reflow) o repintar en cada fotograma. El navegador renderiza en etapas — layout → paint → composite. Animar width, height, top, left, margin, padding, box-shadow o filter re-ejecuta layout y/o paint en cada fotograma, bloqueando el hilo principal. Animar transform y opacity se ejecuta completamente en el compositor (a menudo la GPU), saltándose layout y paint, lo que hace que la animación sea suave a 60/120fps.

Cuándo usarlo

Úsalo cuando una animación tartamudea o pierde fotogramas, cuando un efecto hover/scroll se siente pesado, cuando se anima tamaño/posición/sombra, cuando un cambio de layout necesita animarse suavemente (reordenación de tarjetas, un elemento moviéndose entre contenedores), cuando se anima height: auto, o al revisar código de animación por rendimiento.

Regla principal: animar solo transform y opacity

Mapea cada animación "costosa" a un equivalente barato.

Animando (costoso)DisparaReemplazar con (barato)
width / heightLayout + Painttransform: scaleX()/scaleY() (+ FLIP para tamaño real)
top / left / marginLayout + Painttransform: translate()
box-shadowPaintAnimar opacity de un pseudo-elemento que contenga la sombra
filter: blur()Paint (pesado)Crossfade de dos capas mediante opacity, o aceptar con moderación
background-positionPainttransform: translate() en una capa hija
color / backgroundPaintA menudo aceptable; o crossfade de capas

Posición y tamaño mediante transform

/* MALO: anima layout en cada fotograma */
.box { transition: left 300ms, width 300ms; left: 0; width: 100px; }
.box:hover { left: 200px; width: 200px; }

/* BUENO: solo compositor */
.box {
  transition: transform 300ms ease;
  transform: translateX(0) scaleX(1);
  transform-origin: left center;
}
.box:hover { transform: translateX(200px) scaleX(2); }

scaleX distorsiona el contenido interno (el texto se estira). Para redimensionar verdadero sin distorsión, usa FLIP.

Box-shadow barato mediante opacidad de pseudo-elemento

Animar box-shadow repinta una gran región borrosa en cada fotograma. En su lugar, pinta la sombra una vez en un ::after, luego anima solo su opacity.

.card { position: relative; }
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0 12px 28px rgba(0,0,0,0.35);
  opacity: 0;
  transition: opacity 300ms ease;
  pointer-events: none;
}
.card:hover::after { opacity: 1; }

La sombra borrosa se rasteriza una vez; al hacer hover solo cambia una opacidad del compositor — suave a cualquier tasa de fotogramas.

FLIP: animar cambios de layout de forma barata

FLIP (First, Last, Invert, Play) anima un cambio de layout (reordenación, redimensionamiento, movimiento entre contenedores) usando solo transform. Mide dónde estaba el elemento (First) y dónde estará (Last), aplica una transformación inversa para que visualmente parezca no haberse movido, luego anima la transformación de vuelta a la identidad (Play). El DOM termina en su layout final real; el movimiento es trabajo puro del compositor.

function flip(el, mutate) {
  const first = el.getBoundingClientRect();   // First
  mutate();                                    // cambia el DOM/layout
  const last = el.getBoundingClientRect();     // Last

  const dx = first.left - last.left;
  const dy = first.top - last.top;
  const sx = first.width  / last.width;
  const sy = first.height / last.height;

  el.animate(
    [
      { transformOrigin: 'top left',
        transform: `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})` }, // Invert
      { transformOrigin: 'top left', transform: 'none' },               // Play
    ],
    { duration: 300, easing: 'cubic-bezier(0.2, 0, 0, 1)' }
  );
}

// uso: animar un elemento moviéndose a una nueva posición en la cuadrícula
flip(card, () => targetContainer.appendChild(card));

La API animate() de las Web Animations ejecuta la transformación en el compositor. Para muchos elementos, agrupa todas las lecturas de getBoundingClientRect() antes de cualquier mutación (ver layout thrashing a continuación).

Animando height: auto

height: auto no es interpolable históricamente. Enfoques modernos y alternativos:

/* Moderno (Chrome 129+/navegadores compatibles): optar por palabras clave de tamaño en la animación */
.panel {
  interpolate-size: allow-keywords;   /* habilita animar hacia/desde auto */
  height: 0;
  overflow: clip;
  transition: height 300ms ease;
}
.panel.open { height: auto; }
/* calc-size() también funciona: height: calc-size(auto, size); */
/* Alternativa robusta hoy: CSS grid 1fr -> 0fr */
.wrapper {
  display: grid;
  grid-template-rows: 0fr;            /* colapsado */
  transition: grid-template-rows 300ms ease;
}
.wrapper.open { grid-template-rows: 1fr; }
.wrapper > .content { overflow: hidden; min-height: 0; }

El truco de la cuadrícula anima el tamaño de la fila (amigable para el compositor y ampliamente soportado) y no necesita medición de altura con JS. Usa interpolate-size: allow-keywords / calc-size(auto, size) donde los navegadores objetivo lo soporten; mantén la técnica de la cuadrícula como predeterminada para compatibilidad cruzada.

Evitar layout thrashing (agrupar lecturas, luego escrituras)

Leer una propiedad de layout (offsetWidth, getBoundingClientRect, scrollTop, getComputedStyle) después de una escritura fuerza un reflow síncrono. Intercalar lecturas y escrituras en un bucle ("layout thrashing") puede ejecutar docenas de reflows forzados por fotograma.

// MALO: leer, escribir, leer, escribir... fuerza reflow en cada iteración
items.forEach((el) => {
  const w = el.offsetWidth;          // leer (fuerza layout)
  el.style.width = w * 1.5 + 'px';   // escribir (invalida layout)
});

// BUENO: agrupar todas las lecturas, luego todas las escrituras
const widths = items.map((el) => el.offsetWidth);  // todas las lecturas
items.forEach((el, i) => {                          // todas las escrituras
  el.style.width = widths[i] * 1.5 + 'px';
});

Para trabajo sincronizado con fotogramas, lee en un callback de requestAnimationFrame y aplica las escrituras; bibliotecas como fastdom formalizan esta planificación de lectura/escritura.

will-change: usar con moderación

will-change: transform promueve un elemento a su propia capa de compositor antes de tiempo, evitando un tirón al inicio de la animación. Pero cada capa promovida cuesta memoria de GPU, y el uso excesivo degrada el rendimiento.

.menu { will-change: transform; }   /* solo en elementos que estén por animarse */

Reglas: aplica justo antes de la animación (por ejemplo, al hacer hover/estado del padre), elimínalo después (will-change: auto) cuando esté inactivo, nunca lo apliques indiscriminadamente a muchos elementos, y nunca lo dejes permanentemente en nodos grandes/numerosos. Un truco transform: translateZ(0) hace la misma promoción pero es más difícil de deshacer — prefiere will-change.

Entregar y verificar (HTML autónomo)

Ayudante empaquetado (scripts/): scripts/seek-shot.sh anim.html 0 1.5 3 congela el harness ?t=N y captura pantallas de cada momento; scripts/contact-sheet.sh sheet.png frame-*.png las coloca en mosaico para una revisión de un vistazo. Ver scripts/README.md.

El entregable es un .html autocontenido que se abre directamente en un navegador — el marcado, la animación CSS/JS y un harness de congelación en un solo archivo. Para esta habilidad, la verificación tiene dos aspectos: el fotograma debe verse correcto y ser barato de producir (solo compositor).

Contrato de salida:

  • Un .html, dependencias vía CDN si las hay, animación impulsada por transiciones CSS/@keyframes o la API Web Animations.
  • Un mecanismo de congelación para que una captura de pantalla caiga en un fotograma determinista:
    • CSS @keyframes → ?t=N establece el.style.animationDelay = (-N)+'s'; el.style.animationPlayState = 'paused'.
    • WAAPI / JS → conserva el objeto de animación y anim.pause(); anim.currentTime = N*1000.

Bucle de verificación: congelar → captura de pantalla, luego perfilar para jank:

  1. Abre sin cabeza al inicio / medio / final (?t=0, medio, final), captura pantallas; confirma que el movimiento es visualmente correcto (sin texto recortado/estirado por scaleX, FLIP aterriza en el layout real, las transiciones de sombra/altura se ven bien).
  2. Confirma que es solo compositor — el objetivo principal de esta habilidad. Ya sea:
    • DevTools → Performance: graba la animación, verifica que no hay banda púrpura "Layout" ni verde "Paint" por fotograma (solo "Composite Layers").
    • O traza sin cabeza: npx playwright screenshot para lo visual, más una captura CDP/tracing, y afirma que no se disparan eventos Layout/Paint durante la ventana de animación.
  3. Observa que el FPS/"Frame Rendering Stats" se mantiene en 60 — los fotogramas perdidos significan que se ha deslizado una propiedad costosa.
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/anim.html?t=0.3" frame.png

Antes de finalizar:

  1. Se abre de forma autónoma — sin errores en consola.
  2. Solo transform / opacity animan por fotograma; DevTools no muestra Layout/Paint por fotograma.
  3. Captura de pantalla al inicio / medio / final — correcto, sin distorsión de texto por scaleX, FLIP aterriza verdadero.
  4. Mantiene 60fps; will-change aplicado justo a tiempo y eliminado cuando está inactivo (sin capas sobrantes).
  5. prefers-reduced-motion respetado.

Referencia rápida

ObjetivoHaz esto
Mover elementotransform: translate()
Redimensionar sin distorsiónFLIP con transform
Sombra al hoverAnimar opacity del pseudo-elemento de sombra
Expandir a la altura del contenidogrid 0fr→1fr, o interpolate-size: allow-keywords
Muchos elementos moviéndoseAgrupar lecturas de getBoundingClientRect, luego escrituras
Inicio suave de animaciónwill-change justo a tiempo, eliminar cuando esté inactivo
Verificar que es solo compositorDevTools Performance: sin "Layout" púrpura/"Paint" verde por fotograma

Advertencias

  • scaleX/scaleY estira texto e hijos; usa FLIP cuando el contenido deba permanecer nítido.
  • Los porcentajes en transform son relativos al propio cuadro del elemento, no al padre — diferente de left: %.
  • El uso excesivo de will-change o translateZ(0) crea demasiadas capas y perjudica el rendimiento; promociona solo lo que se anima.
  • filter y backdrop-filter están relacionados con el compositor pero siguen siendo costosos; anima su presencia mediante crossfade de opacidad en lugar de animar el radio de desenfoque.
  • El contenido del grid 1fr→0fr debe tener overflow: hidden y min-height: 0 o no colapsará.
  • Siempre limita el movimiento no esencial detrás de @media (prefers-reduced-motion: reduce).

Archivos de referencia

  • references/patterns-and-profiling.md — ejemplos completos ejecutables (acordeón, lista reordenable, paralaje), guía de perfilado en DevTools para confirmar fotogramas solo de compositor, patrones de movimiento reducido y una hoja de trucos de coste de propiedades.

Skills relacionados