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) | Dispara | Reemplazar con (barato) |
|---|---|---|
width / height | Layout + Paint | transform: scaleX()/scaleY() (+ FLIP para tamaño real) |
top / left / margin | Layout + Paint | transform: translate() |
box-shadow | Paint | Animar 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-position | Paint | transform: translate() en una capa hija |
color / background | Paint | A 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 3congela el harness?t=Ny captura pantallas de cada momento;scripts/contact-sheet.sh sheet.png frame-*.pnglas coloca en mosaico para una revisión de un vistazo. Verscripts/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/@keyframeso la API Web Animations. - Un mecanismo de congelación para que una captura de pantalla caiga en un fotograma determinista:
- CSS
@keyframes→?t=Nestableceel.style.animationDelay = (-N)+'s'; el.style.animationPlayState = 'paused'. - WAAPI / JS → conserva el objeto de animación y
anim.pause(); anim.currentTime = N*1000.
- CSS
Bucle de verificación: congelar → captura de pantalla, luego perfilar para jank:
- Abre sin cabeza al inicio / medio / final (
?t=0, medio, final), captura pantallas; confirma que el movimiento es visualmente correcto (sin texto recortado/estirado porscaleX, FLIP aterriza en el layout real, las transiciones de sombra/altura se ven bien). - 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 screenshotpara lo visual, más una captura CDP/tracing, y afirma que no se disparan eventosLayout/Paintdurante la ventana de animación.
- 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:
- Se abre de forma autónoma — sin errores en consola.
- Solo
transform/opacityaniman por fotograma; DevTools no muestra Layout/Paint por fotograma. - Captura de pantalla al inicio / medio / final — correcto, sin distorsión de texto por
scaleX, FLIP aterriza verdadero. - Mantiene 60fps;
will-changeaplicado justo a tiempo y eliminado cuando está inactivo (sin capas sobrantes). prefers-reduced-motionrespetado.
Referencia rápida
| Objetivo | Haz esto |
|---|---|
| Mover elemento | transform: translate() |
| Redimensionar sin distorsión | FLIP con transform |
| Sombra al hover | Animar opacity del pseudo-elemento de sombra |
| Expandir a la altura del contenido | grid 0fr→1fr, o interpolate-size: allow-keywords |
| Muchos elementos moviéndose | Agrupar lecturas de getBoundingClientRect, luego escrituras |
| Inicio suave de animación | will-change justo a tiempo, eliminar cuando esté inactivo |
| Verificar que es solo compositor | DevTools Performance: sin "Layout" púrpura/"Paint" verde por fotograma |
Advertencias
scaleX/scaleYestira texto e hijos; usa FLIP cuando el contenido deba permanecer nítido.- Los porcentajes en
transformson relativos al propio cuadro del elemento, no al padre — diferente deleft: %. - El uso excesivo de
will-changeotranslateZ(0)crea demasiadas capas y perjudica el rendimiento; promociona solo lo que se anima. filterybackdrop-filterestá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→0frdebe teneroverflow: hiddenymin-height: 0o 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.