Communitygithub.com

yokyznt/60fps-animation

Esta habilidade deve ser usada quando o usuário pedir para "corrigir animação CSS travada", "fazer animação rodar a 60fps", "parar layout thrashing", "animar largura/altura/topo/esquerda suavemente", "converter animação para transform", "animar box-shadow de forma performática", "animar height: auto", "animação FLIP", ou "por que minha animação de scroll/hover está travada".

O que é 60fps-animation?

Esta skill resolve a causa número 1 de animações web travadas: animar propriedades que forçam o navegador a recalcular layout ou repintar a cada frame. O foco é ensinar desenvolvedores a usar apenas `transform` e `opacity`, que rodam no compositor (geralmente na GPU), pulando as fases caras de layout e pintura — garantindo animações suaves a 60fps ou mais. A habilidade cobre desde substituições simples (ex: `width` por `transform: scaleX()`) até técnicas avançadas como FLIP para animar mudanças de layout sem distorcer conteúdo, animação de `height: auto` com grid CSS, e pseudo-elementos para `box-shadow` performático. Inclui também práticas para evitar layout thrashing, uso criterioso de `will-change`, e um fluxo de verificação completo com screenshots e profiling no DevTools para confirmar que a animação roda apenas no compositor. É compatível com qualquer plataforma web e se aplica a ferramentas como Claude Code, Cursor, Codex ou qualquer ambiente onde se escreva CSS/JS para animações.

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

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

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)DisparaSubstitua por (barato)
width / heightLayout + Painttransform: scaleX()/scaleY() (+ FLIP para tamanho real)
top / left / marginLayout + Painttransform: translate()
box-shadowPaintAnime 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-positionPainttransform: translate() em uma camada filha
color / backgroundPaintGeralmente 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 3 congela o harness ?t=N e captura telas de cada momento; scripts/contact-sheet.sh sheet.png frame-*.png os organiza em mosaico para revisão rápida. Veja scripts/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/@keyframes ou pela Web Animations API.
  • Um mecanismo de congelamento para que uma captura de tela atinja um frame determinístico:
    • CSS @keyframes → ?t=N define el.style.animationDelay = (-N)+'s'; el.style.animationPlayState = 'paused'.
    • WAAPI / JS → mantenha o objeto de animação e anim.pause(); anim.currentTime = N*1000.

Loop de verificação — congele → capture tela, depois perfile para jank:

  1. Abra headless no início / meio / fim (?t=0, meio, fim), capture telas; confirme que o movimento está visualmente correto (sem texto cortado/esticado de scaleX, FLIP atinge o layout real, transições de sombra/altura parecem corretas).
  2. 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 screenshot para o visual, mais uma captura CDP/tracing, e afirme que nenhum evento Layout/Paint dispara durante a janela da animação.
  3. 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:

  1. Abre autônomo — sem erros no console.
  2. Apenas transform / opacity animam por frame; DevTools não mostra Layout/Paint por frame.
  3. Capturado em início / meio / fim — correto, sem distorção de texto por scaleX, FLIP atinge o layout real.
  4. Mantém 60fps; will-change aplicado no momento certo e removido quando ocioso (sem camadas residuais).
  5. prefers-reduced-motion respeitado.

Referência rápida

ObjetivoFaça isto
Mover elementotransform: translate()
Redimensionar sem distorçãoFLIP com transform
Sombra ao passar o mouseAnime opacity do pseudo-elemento da sombra
Expandir para altura do conteúdogrid 0fr→1fr, ou interpolate-size: allow-keywords
Muitos elementos se movendoAgrupe leituras de getBoundingClientRect, depois escritas
Início suave da animaçãowill-change no momento certo, remova quando ocioso
Verificar se é apenas compositorDevTools Performance: sem "Layout" roxo/"Paint" verde por frame

Pegadinhas

  • scaleX/scaleY estica texto e filhos; use FLIP quando o conteúdo precisar permanecer nítido.
  • Porcentagens de transform são relativas à própria caixa do elemento, não ao pai — diferente de left: %.
  • Usar demais will-change ou translateZ(0) cria muitas camadas e prejudica a performance; promova apenas o que anima.
  • filter e backdrop-filter sã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→0fr deve ter overflow: hidden e min-height: 0 ou 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.

Habilidades Relacionadas