Criando animações perfeitas com CSS animation e transition

Animações perfeitas dão aquele toque elegante em um site, não é mesmo? Você toca em um botão, um movimento suave acontece; abre o menu, uma transição perfeita te leva para outro contexto. Desde que o CSS3 virou algo suportado pelos navegadores mais utilizados, a web tem ótimas ferramentas para criar animações (animation) e transições (transition). Mesmo que essas ferramentas já estejam entre a gente por um tempão, ainda é difícil criar interfaces de usuário com movimentos perfeitos.

Nossos olhos são muito bons em perceber movimento e nossa cabeça é muito boa em sinalizar quando algo não está funcionando do jeito certo. Como a maior parte das nossas telas é capaz de reproduzir 60 frames por segundo (fps), nós estamos muito acostumados a ver essa taxa de atualização em todos os movimentos que acontecem em interfaces de usuário. Para nossas animações serem perfeitas, elas precisam se encaixar nessa taxa de atualização. Vamos entender o porquê.

Índice

O que faz uma animação ou transição ser perfeita?

Em uma tela que atualiza sua imagem a uma taxa de 60fps, o navegador precisa calcular onde cada elemento do seu site aparecerá na tela a cada frame. Para ser perfeita, o movimento da sua animação precisa acontecer a cada 16ms (1000 milissegundos / 60 frames = 16,66). Isso significa que, nesses 16ms, o navegador precisa executar seu código, verificar qualquer mudança na posição de todos os elementos e calcular onde eles estarão.

Exemplo de animação a 60 frames por segundo

Para sites que não mudam a posição de seus elementos, o navegador consegue evitar trabalho e calcular apenas uma vez onde cada pedaço estará. Quando acionamos animações, o navegador precisa calcular a posição dos nossos elementos a cada frame — a cada intervalo de 16ms. Entender esse processo de computação e renderização é essencial para criar animações e transições perfeitas com CSS usando animation e transition.

Toda vez que o cálculo para renderizar o novo frame de uma animação dura mais do que o tempo para atualizar a tela, nós “perdemos frames”. Isso significa que um frame deixou de ser renderizado. A perda de frames é o que tira a fluidez de uma animação e traz uma experiência ruim para o usuário.

Exemplo de quando um frame com 25ms é perdido

Fluxo de renderização do navegador

Todos os navegadores atuais (Chrome, Firefox, Safari, Edge) adotam um fluxo de renderização parecido. Esse fluxo (rendering pipeline, em inglês) é dividido em quatro fases:

  1. Style: Na primeira fase, o navegador calcula quais serão os valores de cada propriedade CSS de cada elemento da tela.
  2. Layout: Com os valores em mãos, calcula-se a disposição de todos os elementos na tela.
  3. Paint: O navegador agora cria instruções de renderização de todos os elementos dividos em cada camadas.
  4. Composite: Por fim, compõe todas as camadas na tela.

Como todo esse processo precisa se encaixar em 16ms durante uma animação para que ela não “engasgue”, todos os navegadores modernos possuem otimizações para passar a menor quantidade de tempo em cada fase ou até pulá-las. Para criar animações performáticas precisamos evitar trabalho.

Fluxo de renderização de um navegador. Style, Layout, Paint, Composite

Na fase Style, o navegador basicamente calcula qual é o estilo a ser computado para cada elemento na página, cada nó do DOM. Por não envolver computação pesada, esta fase geralmente é bem rápida e não representa um problema de performance.

Como evitar trabalho durante uma animação CSS

Após saber o estilo a ser aplicado em cada elemento, o navegador pode iniciar a fase de Layout. Nela será calculada a disposição de cada elemento e a ordem em que eles serão renderizados na página. Em sites com árvores do DOM muito grandes, a fase de Layout pode demorar um tempo considerável e prejudicar a taxa de frames por segundo em uma animação.

Existem várias propriedades CSS que, quando mudadas, podem levar o navegador a iniciar a fase de Layout. width, height, padding e margin são algumas delas que tendemos a animar com mais frequência. Como uma mudança em um elemento no meio da página pode afetar a disposição de toda a página, animar essas propriedades pode gerar um grande trabalho a cada frame. Se possível, evite animar essas propriedades.

A fase de Layout gera uma representação da página em uma Árvore de Layout.

Exemplo de uma Árvore de Layout criada a partir de um site

Após calcular o posicionamento dos elementos, o navegador iniciará a fase de Paint. Aqui, o navegador calcula em que ordem todos os elementos devem ser renderizados, camada por camada, gerando uma representação dos comandos necessários para mostrar o que o usuário verá na tela. A Árvore de Layout é transformada em Registros de Paint.

Exemplo de uma Árvore de Layout transformada em registros de paint

Esta fase pode demandar bastante recurso, então o navegador tenta ao máximo diminuir a área da tela que precisa ser computada. Caso nenhum elemento tenha mudado sua aparência na tela, a fase de Paint será pulada.

As propriedades que geralmente animamos que sempre demandam uma fase de Paint são as que controlam posicionamento (top, left, right, bottom), tamanho (width, height, padding, margin) e cor (background, color).

Criando animações de alta-performance com Composite Layers

Após a fase de Layout, o navegador precisa transformar os Registros de Paint em pixels. Este processo se chama de rasterização e ocorre durante a fase Composite (presente na grande maioria dos navegadores atuais).

Nesta fase, o navegador rasteriza diferentes camadas da interface de usuário e usa a GPU para compor estas camadas e tirar um pouco do peso do cálculo da CPU.

Exemplo da rasterização de cada camada

Para otimizar o uso de recursos do seu computador, o Chrome rasteriza não só a parte que você está vendo na tela, mas boa parte dos elementos da página. E, em vez de criar apenas uma imagem com a composição dos elementos, o Chrome divide certos elementos e camadas diferentes, permitindo que a composição final seja feita na GPU.

Como os cálculos da fase Composite rodam na thread de Composition e não influenciam na performance da thread principal, animações que usam a fase Composite são muito mais performáticas do que as que dependem das fases de Layout e Paint. Isto se deve a evitar a frequente e pesada rasterização de camadas sem necessidade.

Exemplos de camadas rasterizadas

Se não devemos animar width ou margin e é bom evitar mudanças em background e top, o que usar então? Que bom que você perguntou.

As propriedades que, quando animadas, geram camadas que serão usadas na fase Composite são:

Exemplos de animação com transform: translate3d(20px, 20px, 0)

Na minha experiência, elas são mais do que suficientes para boa parte das animações que criamos para melhorar a experiência do usuário.

Boa parte dos computadores e celulares têm GPUs boas o suficiente para criarmos animações bastante complexas com opacity e transform.

Visualizando/debugando animações e o fluxo de renderização do navegador

O Chrome possui uma incrível ferramenta que pode nos ajudar a entender todo o fluxo de renderização e o que acontece durante animações. Essa ferramenta está disponível no Chrome Developer Tools (DevTools), na aba Performance.

Ao clicar no botão de gravação, o DevTools irá guardar dados de tudo que está acontecendo na tela e por baixo dos panos no fluxo de renderização. Assim que você parar de gravar, a ferramenta ficará recheada de informações para explorarmos:

Aba de Performance do DevTools do Chrome com um relatório de performance do site da Tesla

Para aprender um pouco mais sobre a aba Performance do DevTools, vamos visitar um site bastante bonito que possui animações sutis, mas que fazem toda a diferença durante a navegação: tesla.com.

Na página do Model Y, animações são iniciadas conforme rolamos a página para ver mais conteúdo. Ao gravar essa interação no meu computador, pude perceber algumas animações com problemas e outras perfeitas.

O interessante deste relatório da aba Performance é que podemos ir mais fundo nestes problemas e tentar entendê-lo, mesmo sem conhecer o código-fonte do site.

A linha do tempo no topo do relatório mostra alguns retângulos vermelhos. Ao focar nestes frames com problemas, podemos ver que um deles demorou cerca de 146ms para ser renderizado — um número muito acima do ideal de 16ms.

Frame longo na página do Model Y no site da Tesla

Nas informações da thread principal, podemos ver que a fase de Style ocupou sozinha 52ms. Ela veio logo após uma chamada método scrollTo no JavaScript. Este método gera Layout Trashing, o que força o recálculo dos estilos de todos os elementos no DOM e cálculos da fase de Layout.

Frame longo na página do Model Y no site da Tesla

Depois dessa parte mais pesada na performance, as animações são bem mais leves e performáticas. No quadro abaixo, podemos explorar o quão rápidas animações podem ser quando fazem uso de camadas na fase Composite.

Frame curto de uma animação usando Composite Layers

Neste caso, foram apenas 42 microssegundos necessários para a fase Composite e 2,72 milissegundos para todo o frame.

A (má) influência do JavaScript em animações CSS

Não é só o CSS que influencia no fluxo de renderização e na performance das animações CSS. Código JavaScript também pode influenciar nessas animações, como vimos no exemplo do site da Tesla.

Como as fases de Style, Layout e Paint são executadas na thread principal do navegador, qualquer outro código JavaScript que estiver sendo executado ao mesmo tempo vai tomar espaço dentro do fluxo de renderização.

Além disso, alguns métodos e propriedades no JavaScript afetam diretamente o fluxo de renderização. Esses métodos e propriedades geram o que chamamos de Layout Thrashing. Usar element.getBoundingClientRect(), por exemplo, obriga o navegador a recalcular os estilos do elemento e passar pela fase de Layout para gerar o resultado da chamada deste método.

button.addEventListener("click", (event) => {
  // A chamada deste método gera Layout Thrasing
  console.log(button.getBoundingClientRect());
});

A lista de métodos e propriedades que geram Layout Thrashing é bem extensa e difere um pouco entre diferentes navegadores. O Paul Irish criou o documento What forces layout / reflow que conta com uma lista completa do que você deve evitar.

Alguns deles são:

O Google também documenta a boa prática de evitar Layout Thrashing no artigo Avoid Large, Complex Layouts and Layout Thrashing, que detalha bem algumas coisas que você deve evitar fazer durante animações, como loops que calculam e mudam o tamanho de vários elementos em sequência.

Sua vez de deixar uma animação CSS perfeita

Agora que você conhece todo o fluxo de renderização do navegador, nós podemos criar uma animação CSS perfeita.

E qual a melhor forma de fazer isso? Consertando uma imperfeita, é claro.

No Codepen abaixo, temos uma interface bem simples com um conteúdo principal e um menu lateral. No conteúdo principal, temos uma lista de tweets com uma animação de hover e um botão para abrir e fechar o menu. Ao ser aberto, o menu lateral é animado e desliza da esquerda para a direita, enquanto deixa o conteúdo principal mais escuro.

Dependendo do seu computador, você poderá perceber os problemas de performance nestas animações visualmente.

Caso a animação esteja rodando bem no seu computador, teste algumas coisas:

Agora é a sua vez de forkar esse Codepen e modificar o código para deixar a animação mais otimizada. Como quero que você aprenda na prática, o código não está documentado. Use o DevTools de Performance para explorar os problemas e utilize o seu conhecimento sobre o fluxo de renderização.

Continue aprendendo

Ainda não cobrimos tudo sobre animações no CSS. Aqui vão algumas referências e ferramentas para continuar estudando: