Impedimento de instabilidade para melhor desempenho de renderização

Tom Wiltzius
Tom Wiltzius

Introdução

O ideal é que seu app da Web pareça responsivo e suave ao fazer animações, transições e outros efeitos pequenos de IU. Garantir que esses efeitos não tenham instabilidade pode significar a diferença entre uma sensação "nativa" ou uma sensação desajeitada e mal-acabada.

Este é o primeiro de uma série de artigos que abordam a otimização do desempenho de renderização no navegador. Para começar, vamos explicar por que é difícil ter uma animação suave e o que precisa acontecer para alcançá-la, além de algumas práticas recomendadas fáceis. Muitas dessas ideias foram apresentadas originalmente em "Jank Busters", uma palestra que Nat Duca e eu demos na Google I/O (vídeo) este ano.

Conheça o V-sync

Os jogadores de PC podem conhecer esse termo, mas ele não é comum na Web: o que é v-sync?

Considere o visor do celular: ele é atualizado em intervalos regulares, geralmente (mas não sempre) cerca de 60 vezes por segundo. V-sync (sincronização vertical) refere-se à prática de gerar novos frames somente entre atualizações de tela. Você pode pensar nisso como uma disputa entre o processo que grava dados no buffer da tela e o sistema operacional que lê esses dados para exibi-los. Queremos que o conteúdo dos frames armazenados em buffer mude entre as atualizações, e não durante elas. Caso contrário, o monitor exibirá metade de um frame e metade de outro, causando a "tearing".

Para ter uma animação suave, você precisa de um novo frame para estar pronto sempre que uma atualização de tela ocorrer. Isso gera duas grandes implicações: o tempo para a renderização do frame (ou seja, quando o frame precisa estar pronto) e o orçamento de frames (ou seja, o tempo que o navegador precisa produzir um frame). Você só tem o tempo entre as atualizações de tela para concluir um frame (aproximadamente 16 ms em uma tela de 60 Hz) e quer começar a produzir o próximo frame assim que o último for colocado na tela.

Tempo é tudo: requestAnimationFrame

Muitos desenvolvedores da Web usam setInterval ou setTimeout a cada 16 milissegundos para criar animações. Esse é um problema por várias razões (e discutiremos mais adiante), mas as seguintes questões são particularmente relevantes:

  • A resolução de timer do JavaScript está na ordem de vários milissegundos
  • Dispositivos diferentes têm taxas de atualização diferentes

Lembre-se do problema de tempo para a renderização do frame mencionado acima: você precisa de um frame de animação concluído, finalizado com qualquer JavaScript, manipulação de DOM, layout, pintura, etc., para estar pronto antes da próxima atualização da tela. A baixa resolução do timer pode dificultar a conclusão dos frames de animação antes da próxima atualização da tela, mas a variação nas taxas de atualização da tela impossibilita a execução de um timer fixo. Não importa qual é o intervalo do timer, você vai se afastar lentamente da janela de tempo de um frame e acabar caindo em um. Isso aconteceria mesmo se o timer fosse acionado com precisão de milissegundos, o que não acontece (como os desenvolvedores descobriram). A resolução do timer varia dependendo se a máquina está com bateria ou conectada, pode ser afetada pelas guias em segundo plano que consomem recursos etc. Mesmo que isso seja raro (por exemplo, a cada 16 frames, porque você está atrasado em um milissegundo), você vai perceber que vai perder vários frames por segundo. Você também fará o trabalho para gerar frames que nunca são exibidos, o que desperdiça energia e tempo de CPU que você poderia gastar fazendo outras coisas no aplicativo.

Telas diferentes têm taxas de atualização diferentes: 60 Hz é comum, mas alguns smartphones têm 59 Hz, alguns laptops caem para 50 Hz no modo de baixo consumo de energia e alguns monitores de computador têm 70 Hz.

Quando discutimos o desempenho de renderização, costumamos nos concentrar em quadros por segundo (QPS), mas a variância pode ser um problema ainda maior. Nossos olhos percebem os pequenos problemas irregulares que uma animação mal programada pode produzir.

Use requestAnimationFrame para gerar frames de animação cronometrados corretamente. Ao usar essa API, você estará solicitando ao navegador um frame de animação. Seu callback será chamado quando o navegador produzir um novo frame em breve. Isso acontece independentemente da taxa de atualização.

requestAnimationFrame também tem outras propriedades interessantes:

  • As animações nas guias em segundo plano são pausadas, conservando os recursos do sistema e a duração da bateria.
  • Se o sistema não consegue processar a renderização na taxa de atualização da tela, ele pode limitar as animações e produzir o callback com menos frequência (por exemplo, 30 vezes por segundo em uma tela de 60 Hz). Embora isso diminua pela metade, o frame rate mantém a consistência da animação. E, como mencionado acima, nossos olhos estão muito mais atentos à variação do que ao frame rate. Uma frequência de 30 Hz estável parece melhor do que 60 Hz, que perde alguns quadros por segundo.

O requestAnimationFrame já foi discutido em todo o lugar, então consulte artigos como este da Creative JS para mais informações, mas esse é um primeiro passo importante para suavizar a animação.

Orçamento do frame

Como queremos um novo frame pronto a cada atualização da tela, há apenas tempo entre as atualizações para fazer todo o trabalho de criar um novo frame. Em uma tela de 60 Hz, isso significa que temos cerca de 16 ms para executar todo o JavaScript, executar layout, pintura e qualquer outra ação que o navegador precise fazer para remover o frame. Isso significa que, se o JavaScript dentro do callback requestAnimationFrame demorar mais de 16 ms para ser executado, você não tem esperança de produzir um frame a tempo para a v-sync.

16 ms não é muito tempo. Felizmente, as Ferramentas para desenvolvedores do Chrome podem ajudá-lo a rastrear se você está ultrapassando o orçamento de frames durante o callback requestAnimationFrame.

Abrir a linha do tempo do Dev Tools e gravar a animação em ação mostra rapidamente que ultrapassamos o orçamento ao fazer a animação. Na Linha do tempo, mude para "Frames" e confira:

Uma demonstração com muito layout
Uma demonstração com muito layout

Esses callbacks requestAnimationFrame (rAF) levam mais de 200 ms. Essa é uma ordem de magnitude muito longa para marcar um frame a cada 16 ms. Abrir um desses callbacks longos de rAF revela o que está acontecendo dentro: neste caso, muito layout.

O vídeo de Paul entra em mais detalhes sobre a causa específica da reformulação do layout (está lendo scrollTop) e como evitá-la. O ponto aqui é que você pode mergulhar no callback e investigar o que está demorando tanto.

Uma demonstração atualizada com layout muito reduzido
Uma demonstração atualizada com layout muito reduzido

Observe os 16 ms de tempo para a renderização do frame. Esse espaço em branco nos frames é a margem que você tem que fazer mais trabalho (ou deixar que o navegador faça o trabalho que precisa fazer em segundo plano). Esse espaço em branco é algo bom.

Outra fonte de instabilidade

A maior causa de problemas ao tentar executar animações com tecnologia JavaScript é que outras coisas podem atrapalhar o callback de rAF e até mesmo impedir a execução. Mesmo que seu callback rAF seja enxuto e seja executado em apenas alguns milissegundos, outras atividades (como processar um XHR recém-chegado, executar manipuladores de eventos de entrada ou executar atualizações programadas em um timer) podem de repente entrar e ser executadas por qualquer período sem rendimento. Em dispositivos móveis, às vezes, o processamento desses eventos pode levar centenas de milissegundos. Durante esse tempo, a animação fica completamente parada. Chamamos esses engates de animação de instabilidade.

Não há uma fórmula mágica para evitar essas situações, mas existem algumas práticas recomendadas de arquitetura para se preparar para o sucesso:

  • Não faça muito processamento em gerenciadores de entrada. Usar muito JS ou tentar reorganizar a página inteira durante, por exemplo, um gerenciador de onscroll é uma causa muito comum de instabilidade.
  • Envie o máximo possível de processamento (ou seja, qualquer coisa que leve muito tempo para ser executada) ao callback de rAF ou aos Web Workers possível.
  • Se você enviar o trabalho ao callback do rAF, tente dividi-lo para processar apenas um pouco cada quadro ou atrase-o até que uma animação importante tenha terminado. Dessa forma, você pode continuar executando callbacks curtos de rAF e animar suavemente.

Para conferir um ótimo tutorial que aborda como enviar o processamento para callbacks requestAnimationFrame em vez de gerenciadores de entrada, consulte o artigo de Paul Lewis Leaner, Meaner, Faster Animations with requestAnimationFrame.

Animação CSS

O que é melhor que JS leve no seu evento e callbacks de rAF? Sem JS.

Anteriormente, dissemos que não há uma solução perfeita para evitar a interrupção dos seus callbacks de rAF, mas é possível usar uma animação CSS para evitar totalmente a necessidade deles. Especificamente no Google Chrome para Android (e outros navegadores estão trabalhando em recursos semelhantes), as animações de CSS têm a propriedade muito desejada para que o navegador possa executá-las mesmo que o JavaScript esteja em execução.

Há uma instrução implícita na seção acima sobre instabilidade: os navegadores só podem fazer uma coisa por vez. Isso não é estritamente verdade, mas é uma boa suposição que se deve ter: a qualquer momento, o navegador pode estar executando JS, executando layout ou pintando, mas apenas um por vez. Você pode conferir isso na visualização "Linha do tempo" das Ferramentas para Desenvolvedores. Uma das exceções a essa regra são as animações CSS no Google Chrome para Android (e em breve para o Chrome para computadores, mas ainda não).

Quando possível, usar uma animação CSS simplifica o aplicativo e permite que as animações sejam executadas sem problemas, mesmo enquanto o JavaScript está em execução.

  // see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for info on rAF polyfills
  rAF = window.requestAnimationFrame;

  var degrees = 0;
  function update(timestamp) {
    document.querySelector('#foo').style.webkitTransform = "rotate(" + degrees + "deg)";
    console.log('updated to degrees ' + degrees);
    degrees = degrees + 1;
    rAF(update);
  }
  rAF(update);

Se você clicar no botão, o JavaScript será executado por 180 ms, causando instabilidade. Mas se, em vez disso, direcionarmos essa animação com animações CSS, a instabilidade não ocorrerá mais.

(Lembre-se, no momento da redação deste artigo, que a animação CSS não apresenta instabilidade apenas no Google Chrome para Android, não no Chrome para computador.)

  /* tools like Modernizr (http://modernizr.com/) can help with CSS polyfills */
  #foo {
    +animation-duration: 3s;
    +animation-timing-function: linear;
    +animation-animation-iteration-count: infinite;
    +animation-animation-name: rotate;
  }

  @+keyframes: rotate; {
    from {
      +transform: rotate(0deg);
    }
    to {
      +transform: rotate(360deg);
    }
  }

Para ver mais informações sobre o uso de animações CSS, consulte artigos como este no MDN.

Finalização

Resumindo:

  1. Durante uma animação, é importante produzir frames para cada atualização de tela. A animação Vsync causa um enorme impacto positivo na sensação do aplicativo.
  2. A melhor maneira de obter a animação vsync no Chrome e em outros navegadores modernos é usar animação CSS. Quando você precisa de mais flexibilidade do que a animação CSS fornece, a melhor técnica é a animação baseada em requestAnimationFrame.
  3. Para manter as animações de rAF íntegras e felizes, certifique-se de que outros manipuladores de eventos não atrapalhem a execução do callback de rAF e mantenha os callbacks de rAF curtos (menos de 15 ms).

Por fim, a animação em vsync não se aplica apenas a animações de IU simples, mas também à animação Canvas2D, animação WebGL e até mesmo rolagem em páginas estáticas. No próximo artigo desta série, vamos nos aprofundar no desempenho da rolagem pensando nesses conceitos.

Boa animação!

Referências