Animação Web Performática
Elimine a causa número 1 de animações web travadas: animar propriedades que forçam o navegador a recalcular layout (reflow) ou repintar a cada frame. O navegador renderiza em estágios — layout → paint → composite. Animar width, height, top, left, margin, padding, box-shadow ou filter reexecuta layout e/ou paint a cada frame, bloqueando a thread principal. Animar transform e opacity roda inteiramente no compositor (geralmente na GPU), pulando layout e paint, o que torna a animação suave a 60/120fps.
Quando usar
Use quando uma animação tremer ou perder quadros, quando um efeito de hover/scroll parecer pesado, ao animar tamanho/posição/sombra, quando uma mudança de layout precisar animar suavemente (reordenação de cards, elemento se movendo entre contêineres), ao animar height: auto, ou ao revisar código de animação para performance.
Regra central: anime apenas transform e opacity
Mapeie cada animação "cara" para um equivalente barato.
| Animando (caro) | Dispara | Substitua por (barato) |
|---|---|---|
width / height | Layout + Paint | transform: scaleX()/scaleY() (+ FLIP para tamanho real) |
top / left / margin | Layout + Paint | transform: translate() |
box-shadow | Paint | Anime opacity de um pseudo-elemento com a sombra |
filter: blur() | Paint (pesado) | Cross-fade de duas camadas via opacity, ou aceite com moderação |
background-position | Paint | transform: translate() em uma camada filha |
color / background | Paint | Geralmente aceitável; ou cross-fade de camadas |
Posição e tamanho via transform
/* RUIM: anima layout a cada frame */
.box { transition: left 300ms, width 300ms; left: 0; width: 100px; }
.box:hover { left: 200px; width: 200px; }
/* BOM: apenas compositor */
.box {
transition: transform 300ms ease;
transform: translateX(0) scaleX(1);
transform-origin: left center;
}
.box:hover { transform: translateX(200px) scaleX(2); }
scaleX distorce o conteúdo interno (texto estica). Para redimensionamento real sem distorção, use FLIP.
Sombra barata via opacidade de pseudo-elemento
Animar box-shadow repinta uma grande região borrada a cada frame. Em vez disso, pinte a sombra uma vez em um ::after e anime apenas sua 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; }
A sombra borrada é rasterizada uma vez; passar o mouse apenas muda a opacidade no compositor — suave em qualquer taxa de quadros.
FLIP: anime mudanças de layout de forma barata
FLIP (First, Last, Invert, Play) anima uma mudança de layout (reordenação, redimensionamento, movimento entre contêineres) usando apenas transform. Meça onde o elemento estava (First) e estará (Last), aplique uma transformação inversa para que visualmente pareça não ter se movido, depois anime a transformação de volta para identidade (Play). O DOM termina em seu layout final real; o movimento é puro trabalho do compositor.
function flip(el, mutate) {
const first = el.getBoundingClientRect(); // First
mutate(); // muda o 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 um elemento se movendo para uma nova posição no grid
flip(card, () => targetContainer.appendChild(card));
A API animate() do Web Animations roda a transformação no compositor. Para muitos elementos, faça todas as leituras de getBoundingClientRect() antes de qualquer mutação (veja layout thrashing abaixo).
Animando height: auto
height: auto não é interpolável historicamente. Abordagens moderna e fallback:
/* Moderno (Chrome 129+/navegadores compatíveis): permitir palavras-chave de tamanho na animação */
.panel {
interpolate-size: allow-keywords; /* permite animar de/para auto */
height: 0;
overflow: clip;
transition: height 300ms ease;
}
.panel.open { height: auto; }
/* calc-size() também funciona: height: calc-size(auto, size); */
/* Fallback robusto hoje: CSS grid 1fr -> 0fr */
.wrapper {
display: grid;
grid-template-rows: 0fr; /* recolhido */
transition: grid-template-rows 300ms ease;
}
.wrapper.open { grid-template-rows: 1fr; }
.wrapper > .content { overflow: hidden; min-height: 0; }
O truque do grid anima o tamanho da faixa (amigável ao compositor e amplamente suportado) e não precisa de medição JS de altura. Use interpolate-size: allow-keywords / calc-size(auto, size) onde os navegadores alvo suportarem; mantenha a técnica do grid como padrão entre navegadores.
Evite layout thrashing (agrupe leituras, depois escritas)
Ler uma propriedade de layout (offsetWidth, getBoundingClientRect, scrollTop, getComputedStyle) após uma escrita força um reflow síncrono. Intercalar leituras e escritas em um loop ("layout thrashing") pode executar dezenas de reflows forçados por frame.
// RUIM: lê, escreve, lê, escreve... força reflow a cada iteração
items.forEach((el) => {
const w = el.offsetWidth; // lê (força layout)
el.style.width = w * 1.5 + 'px'; // escreve (invalida layout)
});
// BOM: agrupa todas as leituras, depois todas as escritas
const widths = items.map((el) => el.offsetWidth); // todas as leituras
items.forEach((el, i) => { // todas as escritas
el.style.width = widths[i] * 1.5 + 'px';
});
Para trabalho sincronizado com frames, leia em um callback de requestAnimationFrame e aplique escritas; bibliotecas como fastdom formalizam esse agendamento de leitura/escrita.
will-change: use com moderação
will-change: transform promove um elemento para sua própria camada do compositor antecipadamente, evitando uma pausa no início da animação. Mas cada camada promovida custa memória da GPU, e o uso excessivo degrada a performance.
.menu { will-change: transform; } /* apenas em elementos prestes a animar */
Regras: aplique pouco antes da animação (ex: no hover/estado pai), remova depois (will-change: auto) quando ocioso, nunca aplique indiscriminadamente a muitos elementos, e nunca deixe permanentemente em nós grandes/numerosos. Um hack transform: translateZ(0) faz a mesma promoção, mas é mais difícil de desfazer — prefira will-change.
Entregue e verifique (HTML autônomo)
Auxiliar empacotado (
scripts/):scripts/seek-shot.sh anim.html 0 1.5 3congela o harness?t=Ne captura telas de cada momento;scripts/contact-sheet.sh sheet.png frame-*.pngos organiza em mosaico para revisão rápida. Vejascripts/README.md.
O entregável é um .html autocontido que abre diretamente no navegador — a marcação, a animação CSS/JS e um harness de congelamento em um único arquivo. Para esta habilidade, a verificação é dupla: o frame deve parecer correto e ser barato de produzir (apenas compositor).
Contrato de saída:
- Um
.html, dependências via CDN se houver, animação conduzida por transições CSS/@keyframesou pela Web Animations API. - Um mecanismo de congelamento para que uma captura de tela atinja um frame determinístico:
- CSS
@keyframes→?t=Ndefineel.style.animationDelay = (-N)+'s'; el.style.animationPlayState = 'paused'. - WAAPI / JS → mantenha o objeto de animação e
anim.pause(); anim.currentTime = N*1000.
- CSS
Loop de verificação — congele → capture tela, depois perfile para jank:
- Abra headless no início / meio / fim (
?t=0, meio, fim), capture telas; confirme que o movimento está visualmente correto (sem texto cortado/esticado descaleX, FLIP atinge o layout real, transições de sombra/altura parecem corretas). - Confirme que é apenas compositor — o ponto central desta habilidade. Ou:
- DevTools → Performance: grave a animação, verifique se não há banda roxa "Layout" ou verde "Paint" por frame (apenas "Composite Layers").
- Ou trace headless:
npx playwright screenshotpara o visual, mais uma captura CDP/tracing, e afirme que nenhum eventoLayout/Paintdispara durante a janela da animação.
- Observe que o overlay de FPS/"Frame Rendering Stats" permanece em 60 — quadros perdidos significam que uma propriedade cara voltou.
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/anim.html?t=0.3" frame.png
Antes de finalizar:
- Abre autônomo — sem erros no console.
- Apenas
transform/opacityanimam por frame; DevTools não mostra Layout/Paint por frame. - Capturado em início / meio / fim — correto, sem distorção de texto por
scaleX, FLIP atinge o layout real. - Mantém 60fps;
will-changeaplicado no momento certo e removido quando ocioso (sem camadas residuais). prefers-reduced-motionrespeitado.
Referência rápida
| Objetivo | Faça isto |
|---|---|
| Mover elemento | transform: translate() |
| Redimensionar sem distorção | FLIP com transform |
| Sombra ao passar o mouse | Anime opacity do pseudo-elemento da sombra |
| Expandir para altura do conteúdo | grid 0fr→1fr, ou interpolate-size: allow-keywords |
| Muitos elementos se movendo | Agrupe leituras de getBoundingClientRect, depois escritas |
| Início suave da animação | will-change no momento certo, remova quando ocioso |
| Verificar se é apenas compositor | DevTools Performance: sem "Layout" roxo/"Paint" verde por frame |
Pegadinhas
scaleX/scaleYestica texto e filhos; use FLIP quando o conteúdo precisar permanecer nítido.- Porcentagens de
transformsão relativas à própria caixa do elemento, não ao pai — diferente deleft: %. - Usar demais
will-changeoutranslateZ(0)cria muitas camadas e prejudica a performance; promova apenas o que anima. filterebackdrop-filtersão relacionados ao compositor, mas ainda caros; anime sua presença via cross-fades de opacidade em vez de animar o raio de desfoque.- O conteúdo do grid
1fr→0frdeve teroverflow: hiddenemin-height: 0ou não colapsará. - Sempre bloqueie movimentos não essenciais atrás de
@media (prefers-reduced-motion: reduce).
Arquivos de referência
references/patterns-and-profiling.md— exemplos completos executáveis (acordeão, lista reordenável, parallax), passo a passo de profiling no DevTools para confirmar frames apenas no compositor, padrões de movimento reduzido e uma folha de dicas de custo de propriedades.