Impedimento de instabilidade para melhor desempenho de renderização

Tom Wiltzius
Tom Wiltzius

Introdução

Você quer que seu aplicativo da Web seja responsivo e suave ao fazer animações, transições e outros efeitos pequenos de interface. Garantir que esses efeitos não tenham instabilidade pode significar a diferença entre uma versão parecer ou desajeitada e mal-acabada.

Este é o primeiro de uma série de artigos que abordam a otimização de desempenho da renderização no navegador. Para começar, vamos abordar por que uma animação suave é difícil 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", Nat Duca e eu demos uma palestra no Google I/O (vídeo) este ano.

Conheça o V-sync

Gamers de PC podem estar familiarizados com esse termo, mas ele não é comum na Web: o que é v-sync?

Considere a tela do smartphone: ele é atualizado em um intervalo regular, geralmente (mas nem sempre) cerca de 60 vezes por segundo. A V-sync (sincronização vertical) se refere à prática de gerar novos frames somente entre as atualizações de tela. Você pode pensar nisso como uma disputa entre o processo que grava dados no buffer de tela e o sistema operacional que lê esses dados para colocá-los na tela. Queremos que o conteúdo do frame armazenado em buffer mude entre essas atualizações, não durante elas. Caso contrário, o monitor vai exibir metade de um frame e metade de outro, o que causa uma ruptura.

Para ter uma animação suave, é necessário ter um novo frame pronto sempre que uma atualização de tela ocorrer. Isso tem duas grandes implicações: tempo para a renderização do frame (quando o frame precisa estar pronto) e orçamento de frames (o tempo que o navegador tem para produzir um frame). Você só tem 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 exibido na tela.

O tempo é tudo: requestAnimationFrame

Muitos desenvolvedores Web usam setInterval ou setTimeout a cada 16 milissegundos para criar animações. Isso ocorre por diversos motivos (e discutiremos mais sobre isso em breve). No entanto, as principais preocupações são:

  • A resolução do timer do JavaScript é de apenas alguns milissegundos.
  • Dispositivos diferentes têm taxas de atualização distintas

Lembre-se do problema de tempo para a renderização do frame mencionado acima: você precisa de um frame de animação completo, finalizado com qualquer JavaScript, manipulação de DOM, layout, pintura, etc., para estar pronto antes que a próxima atualização de tela ocorra. A resolução baixa do timer pode dificultar a conclusão de frames de animação antes da próxima atualização da tela, mas a variação nas taxas de atualização da tela torna isso impossível com um timer fixo. Não importa qual é o intervalo do timer, você sairá lentamente da janela de tempo de um frame e acabará descartando um. Isso aconteceria mesmo se o timer fosse disparado com uma precisão de milissegundos, o que não seria possível (como os desenvolvedores descobriram). A resolução do timer varia dependendo se a máquina está usando a bateria ou conectada, pode ser afetada por guias em segundo plano que ocupam recursos etc. Mesmo que isso seja raro (digamos, a cada 16 quadros devido à inatividade em um milissegundo), você vai perceber que haverá uma queda de vários milissegundos. Você também fará o trabalho de gerar frames que nunca são exibidos, o que desperdiça energia e tempo de CPU que você poderia gastar fazendo outras coisas em seu 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 desktop têm 70 Hz.

A tendência é nos concentrarmos nos quadros por segundo (QPS) quando falamos sobre o desempenho da renderização, mas a variância pode ser um problema ainda maior. Nossos olhos percebem os pequenos problemas irregulares na animação que uma animação mal cronometrada pode produzir.

A maneira de conseguir frames de animação cronometrados corretamente é usar requestAnimationFrame. Ao usar essa API, você está solicitando um frame de animação ao navegador. 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 boas:

  • 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 (digamos, 30 vezes por segundo em uma tela de 60 Hz). Embora isso reduza o frame rate pela metade, ele mantém a animação consistente. Como mencionado acima, nossos olhos estão muito mais sintonizados com a variação do que com o frame rate. Um 30 Hz constante parece melhor do que 60 Hz que perde alguns quadros por segundo.

O requestAnimationFrame já foi discutido o tempo todo. Consulte artigos como este do JS criativo para mais informações sobre ele, mas é um primeiro passo importante para suavizar a animação.

Orçamento do frame

Como queremos um novo frame pronto em cada atualização de tela, há apenas o tempo entre as atualizações para fazer todo o trabalho de criação de um novo frame. Em uma tela de 60 Hz, isso significa que temos cerca de 16 ms para executar todo o JavaScript, executar o layout, a pintura e o que mais o navegador tiver que fazer para conseguir o frame. Isso significa que, se o JavaScript dentro do callback requestAnimationFrame levar mais de 16 ms para ser executado, você não terá nenhuma 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 ajudar a rastrear se você está ultrapassando o orçamento de frames durante o retorno de requestAnimationFrame.

Abrir a linha do tempo do Dev Tools e gravar dessa animação em ação rapidamente mostra que estamos muito acima do orçamento na animação. Mudança na linha do tempo para "Frames" e confira:

Uma demonstração com excesso de layout
Uma demonstração com excesso de layout

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

O vídeo de Paul aborda mais detalhes sobre a causa específica do novo layout (lendo scrollTop) e como evitá-lo. Mas o ponto aqui é que você pode investigar o 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 tempos para a renderização do frame de 16 ms. Esse espaço em branco nos frames é a margem que você tem que trabalhar (ou deixar que o navegador faça o trabalho que precisa fazer em segundo plano). Esse espaço em branco é uma coisa boa.

Outra fonte de instabilidade

O maior problema ao tentar executar animações com JavaScript é que outras coisas podem atrapalhar o retorno de chamada de rAF e até mesmo impedi-lo de serem executados. Mesmo que o callback de rAF seja enxuto e executado em apenas alguns milésimos de segundo, outras atividades (como processar um XHR recém-chegado, execução de manipuladores de eventos de entrada ou de atualizações programadas em um temporizador) entram e são executados repentinamente por qualquer período sem rendimento. Em dispositivos móveis às vezes o processamento desses eventos pode levar centenas de milissegundos. Durante o qual a animação ficará completamente parada. Eles são chamados a instabilidade da animação.

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

  • Não realize muito processamento nos gerenciadores de entrada. Fazer muito JavaScript ou tentar reorganizar a página inteira durante, por exemplo, um gerenciador de on-scroll é uma causa muito comum de instabilidades terríveis.
  • Envie o máximo possível de processamento (ou seja, tudo o que levará muito tempo para ser executado) no callback de rAF ou nos Web Workers.
  • Se você empurrar o trabalho para o retorno de chamada de rAF, tente dividi-lo para que processe apenas uma pequena parte de cada quadro ou atrase-o até que uma animação importante termine. Dessa forma, você pode continuar a executar callbacks curtos de rAF e animar suavemente.

Para ver um ótimo tutorial sobre 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 do que JS leve no seu evento e callbacks de rAF? Sem JS.

Anteriormente, dissemos que não existe uma solução perfeita para evitar a interrupção dos callbacks de rAF, mas você pode usar a animação CSS para evitar totalmente a necessidade deles. Particularmente no Google Chrome para Android (e outros navegadores estão trabalhando em recursos semelhantes), as animações CSS têm a propriedade desejável de que o navegador possa executá-las com frequência, mesmo que o JavaScript esteja em execução.

Há uma declaração implícita na seção acima sobre instabilidade: os navegadores só podem fazer uma coisa de cada vez. Isso não é estritamente verdade, mas é uma boa suposição de trabalho a ter: a qualquer momento, o navegador pode executar JS, executar layout ou pintura, mas apenas um por vez. Isso pode ser verificado na visualização da 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 no Chrome para computadores, mas ainda não).

Quando possível, o uso de uma animação CSS simplifica o aplicativo e permite que as animações sejam executadas sem problemas, mesmo durante a execução do JavaScript.

  // 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, o que vai causar instabilidade. No entanto, se, em vez disso, usarmos animações em CSS na mesma animação, a instabilidade não vai ocorrer mais.

No momento em que este artigo foi escrito, a animação CSS só não apresenta instabilidade no Chrome para Android, e 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 mais informações sobre o uso de animações CSS, consulte artigos como este no MDN.

Conclusão

Em resumo:

  1. Na animação, é importante produzir frames para cada atualização de tela. A animação do Vsync tem um grande impacto positivo na experiência do app.
  2. A melhor maneira de conseguir animações com vsync no Chrome e em outros navegadores modernos é para usar animação CSS. Quando você precisar de mais flexibilidade que animação CSS. fornece, a melhor técnica é a animação baseada em requestAnimationFrame.
  3. Para manter as animações de rAF saudáveis e satisfatórias, certifique-se de que outros manipuladores de eventos não atrapalhem a execução do callback de rAF e mantenham os callbacks de rAF curto (<15ms).

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

Boa animação!

Referências