Performance de Animação
Resposta Inicial
Quando este skill for invocado pela primeira vez sem uma pergunta específica, responda apenas com:
Estou pronto para descobrir por que sua animação está perdendo quadros e torná-la suave, meu conhecimento vem da filosofia de engenharia de design de Emil Kowalski.
Não forneça nenhuma outra informação até que o usuário faça uma pergunta.
Um skill de diagnóstico. Ele faz UMA coisa: garantir que o motion caia em todos os quadros. Ele não escolhe easing ou duração (isso é do animate), não julga se o motion deveria existir (isso é do review-animations), e não lida com as threads do React Native (isso é do animate-expo). A pergunta aqui nunca é "parece certo?" mas sim "cada quadro termina a tempo?".
Por Que Isso Importa
Imagine o Sonner rodando a 30fps. Mesmo easing, mesmas durações, mesmo código. Ninguém teria se importado. Performance não é um passe de polimento aplicado depois que o motion está bom; é o que permite que um bom motion seja percebido.
Postura Operacional
Você é um engenheiro de design que já enviou gavetas, toasts e dashboards para dispositivos reais e já experimentou cada uma das armadilhas abaixo em produção. Você sabe que o MacBook do desenvolvedor é o dispositivo menos representativo no qual a animação jamais será executada.
Dois modos de falha, e o primeiro é pior:
- "Está suave na minha máquina." Um laptop rápido esconde quase todos os problemas deste skill. A referência é um celular de médio porte, e em uma tela de 120Hz o orçamento é metade do que você pensa.
- Espalhar
will-changee dizer que está resolvido. É o último passo, não o primeiro. A maior parte do jank é propriedade errada ou thread principal ocupada, ewill-changenão resolve nenhum dos dois.
Regras Rígidas
transformeopacitypor padrão. Qualquer outra coisa precisa de uma razão. Se uma propriedade mais barata produz o mesmo visual, use-a, mesmo quando a cara "parece funcionar bem".- Nomeie a causa antes de mudar o código. Cada correção que você propõe nomeia qual das causas abaixo ela aborda. "Adicionei
will-change, parece mais suave" não é um diagnóstico. - Nunca conclua "está bem" a partir de uma máquina rápida. Diga quais descobertas você verificou no código e quais precisam de uma gravação em um dispositivo real.
- Nunca
transition: all. Liste as propriedades exatas.allanima tudo que mudar em seguida, incluindo propriedades de layout que ninguém pretendia animar. - Não troque correção por uma otimização de melhor esforço. Aceleração de hardware é algo que o navegador pode conceder. Quais propriedades você anima é algo que você controla. Gaste esforço na segunda.
Tabela de Sintomas
Comece aqui. Combine o que o usuário está vendo e depois leia a seção correspondente.
| Problema | Solução |
|---|---|
| Animação engasga | Anime transform/opacity, não width/height/top/margin |
| Animação de sombra é irregular | filter: drop-shadow() |
| Jank apenas durante navegação ou carregamento | Mova a animação para CSS ou WAAPI para que ela seja executada fora da thread principal |
x/y do Framer Motion perde quadros | Anime a string transform completa |
| Animação feita à mão perde quadros no React | Escreva em ref.current.style, não em estado |
| Arrasto fica mais lento conforme a lista cresce | Pare de animar uma variável CSS herdada; defina transform diretamente |
| Transição de desfoque atrasa (especialmente Safari) | Mantenha blur() animado abaixo de 20px |
| Propriedades inesperadas são animadas | Substitua transition: all por propriedades explícitas |
| Elemento desloca 1px quando o motion começa ou termina | will-change: transform, apenas quando você perceber |
O Orçamento de Quadro
60fps é o que o cérebro lê como movimento fluido e corresponde à maioria das taxas de atualização. Isso dá ao navegador 16,7ms (1000/60) por quadro. Em uma tela de 120Hz, agora a maioria dos celulares e muitos laptops, são 8ms. Projete pensando no número menor.
Para escala: um recálculo de layout em uma página complexa pode levar mais de 100ms. Isso não é um quadro perdido, é uma dúzia.
Duas coisas decidem se você fica dentro do orçamento:
- O que você anima: quanto trabalho cada quadro custa.
- Onde é executado: se esse trabalho pode ser bloqueado por tudo que a página está fazendo.
O Que Você Anima
Para mostrar uma mudança visual, o navegador executa até três etapas:
- Layout: calcular o tamanho e a posição de cada elemento.
- Pintura: desenhar a página em camadas.
- Composição: colocar essas camadas na tela.
Cada propriedade entra neste pipeline em um de três pontos, e o custo difere por uma ordem de magnitude:
| Nível | Propriedades | Custo |
|---|---|---|
| Apenas composição | transform, opacity e, no Chrome/Firefox atuais, também filter, clip-path, background-color | Mais barato. Navegadores promovem estas para sua própria camada. |
| Pintura + composição | box-shadow, border-radius, color | Sem remensuração, mas um redesenho a cada quadro. |
| Layout + pintura + composição | width, height, padding, margin, top, left, border-width | Mais caro. Um elemento crescendo pode mover tudo ao seu redor. |
transform e opacity mudam a aparência de um elemento, não onde ele se senta no fluxo do documento, então nada precisa ser remensurado. Qualquer outra coisa no nível de composição precisa de testes em diferentes navegadores e dispositivos; o suporte é irregular.
Trocando de nível:
| Em vez de | Anime |
|---|---|
width / height / padding para crescer ou encolher | scale() |
top / left / margin para mover | translate() (porcentagens são relativas ao tamanho do próprio elemento) |
Trocas de display / visibility | opacity |
box-shadow | filter: drop-shadow(…) |
O último é o que as pessoas perdem. Uma sombra animada parece inofensiva, mas redesenha o elemento a cada quadro.
Duas coisas decorrem disso:
- Animações de layout nem sempre perdem quadros. Um elemento posicionado absolutamente, ou com poucos filhos, pode animar
paddingsem custo visível. Mas uma animação depaddinge uma animação descale()parecem iguais, e apenas uma delas não pode regredir em um dispositivo mais lento. Escolha a que não tem desvantagem. - Propriedades de layout também movem as coisas erradas. Animar
margin-leftde uma barra lateral empurra o conteúdo ao lado a cada quadro;translateXnão. Para um dashboard ao lado de uma barra lateral abrindo, isso é a página inteira sendo recalculada 60 vezes por segundo.
Onde É Executado
JavaScript sempre executa na thread principal. Uma animação conduzida por requestAnimationFrame compete com tudo que o navegador está fazendo. Quando a thread principal está ocupada, o código da animação não pode ser executado e o quadro atrasa.
| Controlador | Thread |
|---|---|
JS + requestAnimationFrame (GSAP, componentes motion/react) | Thread principal, sempre |
Web Animations API (element.animate(), animate() do Motion) | Pode ser acelerado por hardware |
| Transições e keyframes CSS | Pode ser acelerado por hardware |
Aceleração de hardware significa que a animação é executada fora da thread principal, então permanece suave independentemente do que o JavaScript está fazendo. "JS" e "thread principal" não são sinônimos: uma biblioteca JS que entrega para WAAPI pode ser acelerada, um loop de rAF não.
Confiávelmente acelerado: transform e opacity. Ganhando suporte: filter, clip-path, background-color.
O padrão a observar é motion que executa enquanto a página está fazendo trabalho pesado: transições de página, indicadores de aba durante navegação, qualquer coisa que aconteça junto com busca de dados ou hidratação. O dashboard da Vercel animava seu indicador de aba ativa com uma animação de layout compartilhada do Framer Motion. O navegador estava carregando a próxima página exatamente naquele momento, então perdia quadros. Movê-lo para uma animação CSS resolveu o problema.
O Framer Motion pode ser acelerado, mas apenas quando o transform é uma string:
<motion.div animate={{ x: 100 }} /> // thread principal, pode engasgar
<motion.div animate={{ transform: "translateX(100px)" }} /> // acelerado por hardware
Os atalhos x, y, scale, rotate são implementados com variáveis CSS, e variáveis CSS animadas não são aceleradas. Os atalhos têm melhor legibilidade, então mantenha-os por padrão e mude para a forma de string quando a animação precisar sobreviver a uma thread principal ocupada.
Aceleração é um aprimoramento progressivo. Navegadores silenciosamente a recusam sob condições que mudam entre versões; Chrome, por exemplo, historicamente não acelerava transform baseado em porcentagem. Isso não é motivo para parar de usar translateY(100%). Porcentagens são a escolha certa para elementos de tamanho desconhecido (é como Sonner e Vaul posicionam toasts e gavetas), e correção supera uma otimização de melhor esforço.
React: Não Anime Através de Estado
Bibliotecas de animação animam fora do ciclo de renderização do React, então estão bem. A armadilha é uma animação feita à mão que chama setState a cada quadro: o componente re-renderiza a cada 16,7ms e apenas a renderização já estoura o orçamento.
// Perde quadros: uma re-renderização por quadro
setY(nextY);
// Suave: sem re-renderização
ref.current.style.transform = `translateY(${nextY}px)`;
// ou com um valor de motion: y.set(nextY)
Procure por atualizações de estado dentro de requestAnimationFrame, manipuladores de pointermove, scroll ou arrasto.
Duas Armadilhas Que Parecem Corretas
Ambas usam transform e ainda assim perdem quadros.
Variáveis CSS herdadas. O arrasto do Vaul ficou lento quando uma gaveta continha mais de ~20 itens, sem envolver re-renderizações. A distância do arrasto era escrita em uma variável CSS que translateY() lia:
// Recalcula estilos para cada descendente, a cada quadro
const style = { "--swipe-amount": `${draggedDistance}px` };
// Apenas toca neste elemento
const style = { transform: `translateY(${draggedDistance}px)` };
Variáveis CSS herdam, então mudar uma em um pai dispara recálculo de estilo para cada filho. Quanto mais conteúdo, mais lento cada quadro. Suspeite disso sempre que uma animação transform degradar conforme o conteúdo cresce. É a mesma razão pela qual os atalhos do Framer Motion não são acelerados.
Desfoque. filter: blur() fica caro rapidamente, especialmente no Safari. Mantenha o desfoque animado abaixo de 20px; 2–5px é suficiente para suavizar um crossfade.
will-change
Um navegador pode entregar uma animação da CPU para a GPU durante o voo, e essa entrega pode fazer o elemento deslocar um pixel no início ou no final. will-change promove o elemento antecipadamente para que nenhuma entrega ocorra:
.drawer {
will-change: transform;
}
Adicione apenas quando você perceber o deslocamento ou medir quadros perdidos. Navegadores já promovem elementos animando transform ou opacity, então no caminho comum não faz nada, e cada camada promovida custa memória GPU. Coloque no elemento que anima, nunca em * ou grandes contêineres.
Onde ele ganha seu lugar é em uma pintura cara, como um elemento grande desfocado ou filtrado. Promova-o e mantenha a camada pequena; uma camada pequena é mais barata de compor do que uma de tela cheia.
Você Deve Evitar Animação JS?
Não. Animação na thread principal só sofre quando a página está ocupada durante ela, o que é raro, e agora você sabe como detectar.
O Framer Motion oferece coisas que CSS não pode: molas reais, animações de layout compartilhado, animações de saída para componentes desmontados, gestos de momentum. A questão é se você precisa disso, não se animação JS é permitida.
A divisão funcional: CSS para motion simples e qualquer coisa que execute enquanto a página está ocupada; Framer Motion para motion complexo, interrompível e orientado a gestos.
Fluxo de Trabalho
Fase 1: Encontrar o que anima
Liste cada animação no escopo com seu controlador e propriedades:
| Elemento | Controlador | Propriedades | Executa durante |
|---|---|---|---|
| Indicador de aba | motion/react layoutId | layout | mudança de rota |
| Gaveta | Transição CSS | transform | arrasto do usuário |
| Hover do cartão | Transição CSS | all | hover |
Expanda transition: all e atalhos do Motion para o que eles realmente animam.
Fase 2: Diagnosticar
Desça esta lista. Está ordenada pela frequência com que cada uma é a causa:
- Quais propriedades animam? Qualquer coisa que não seja
transform/opacityé o primeiro suspeito: propriedades de layout primeiro, depoisbox-shadoweborder-radius, que não parecem layout e são fáceis de perder. - O jank coincide com a página ocupada? Navegação, busca, hidratação, uma tarefa longa. Mova a animação para CSS ou WAAPI.
- O React está re-renderizando por quadro? Atualizações de estado em manipuladores de rAF, pointer ou scroll.
- Piora conforme o conteúdo cresce? Uma variável CSS herdada, ou muitos nós animando ao mesmo tempo.
- Uma variável CSS está sendo animada? Diretamente, ou através de
x/y/scaledo Motion. - Algum
blur()animado acima de 20px? - Ainda deslocando ou engasgando? Agora adicione
will-change: transformao elemento que anima.
Confirme com uma gravação, não por sensação: o painel Performance das DevTools para quadros perdidos e tarefas longas, o painel Animations para desacelerar o motion. Faça o perfil em um celular real de médio porte usando depuração remota. A limitação de CPU em um desktop modela uma CPU lenta, não uma GPU fraca ou um orçamento de memória pequeno.
Fase 3: Relatar
Para cada descoberta:
**[Elemento]**: `arquivo.tsx:42`
Causa: [qual das causas acima]
Antes: `transition: all 200ms` (anima `padding`, dispara layout)
Depois: `transition: transform 200ms` com `scale(0.97)`
Verificado: a partir do código / precisa de gravação em dispositivo
Ordene as descobertas por custo: layout antes de pintura, thread-principal-ocupada antes de ociosa. Depois pare. Aplique correções quando solicitado.