Como evitar tintas desnecessárias

Introdução

Pintar os elementos de um site ou aplicativo pode ser muito caro e ter um efeito negativo no desempenho em tempo de execução. Neste artigo, vamos dar uma olhada rápida no que pode acionar a pintura no navegador e como você pode evitar que pinturas desnecessárias ocorram.

Pintura: um tour rápido

Uma das principais tarefas que um navegador precisa realizar é converter o DOM e CSS em pixels na tela, através de um processo bastante complexo. Ele começa lendo a marcação e, a partir dela, cria uma árvore do DOM. Ele faz algo semelhante com o CSS e, a partir disso, cria o CSSOM. Em seguida, o DOM e o CSSOM são combinados e, por fim, chegamos a uma estrutura da qual podemos começar a pintar alguns pixels.

O processo de pintura em si é interessante. No Google Chrome, a árvore combinada de DOM e CSS é rasterizada por um software chamado Skia. Se você já jogou com o elemento canvas, a API do Skia parece familiar para você. Existem muitas funções no estilo moveTo e lineTo, além de várias outras mais avançadas. Essencialmente, todos os elementos que precisam ser pintados são destilados para uma coleção de chamadas Skia que podem ser executadas, e a saída é um monte de bitmaps. Esses bitmaps são enviados para a GPU, e ela ajuda a combiná-los para ter a imagem final na tela.

Dom para pixels

É importante lembrar que a carga de trabalho da Skia é diretamente afetada pelos estilos que você aplica aos seus elementos. Se você usar estilos com uso de algoritmos pesados, Skia terá mais trabalho a fazer. Colt McAnlis escreveu um artigo sobre como o CSS afeta o peso da renderização da página, portanto, você deve lê-lo para obter mais informações.

Dito isso, a execução da pintura leva tempo e, se não a reduzirmos, ultrapassaremos nosso orçamento de frames de aproximadamente 16 ms. Os usuários vão perceber que perdemos frames e ver isso como instabilidade, o que acaba prejudicando a experiência do usuário no app. Nós realmente não queremos isso, então vamos conferir que tipos de coisas fazem com que o trabalho de pintura seja necessário e o que podemos fazer a respeito.

Rolagem

Sempre que você rolar para cima ou para baixo no navegador, será necessário repintar o conteúdo antes que ele apareça na tela. Tudo bem, isso será apenas uma pequena área, mas mesmo se for o caso, os elementos que precisam ser desenhados podem ter estilos complexos aplicados. Então, só porque você tem uma área pequena para pintar, não significa que vai acontecer rapidamente.

Para ver quais áreas estão sendo repintadas, você pode usar o recurso “Mostrar retângulos de pintura” no DevTools do Chrome (basta tocar na engrenagem pequena no canto inferior direito). Com o DevTools aberto, basta interagir com a página e você verá retângulos piscando onde e quando o Chrome pintou uma parte da página.

Mostrar retângulos de pintura no Chrome DevTools
Mostrar retângulos de pintura no Chrome DevTools

O desempenho de rolagem é fundamental para o sucesso do seu site. Os usuários realmente percebem quando seu site ou aplicativo não rola bem, e eles não gostam. Portanto, temos interesse em manter o trabalho de pintura leve durante as rolagens para que os usuários não notem instabilidades.

Já escrevi um artigo sobre o desempenho de rolagem, então leia se quiser saber mais sobre os detalhes do desempenho de rolagem.

Interações

As interações são outra causa do trabalho de pintura: passar o cursor, clicar, tocar e arrastar. Sempre que o usuário realizar uma dessas interações, por exemplo, uma ação de passar o cursor, o Chrome terá que repintar o elemento afetado. Assim como acontece na rolagem, se houver uma pintura grande e complexa, você vai notar uma queda no frame rate.

Todo mundo quer animações de interação agradáveis e suaves, portanto, novamente, precisaremos ver se os estilos que mudam em nossa animação estão nos custando muito tempo.

Uma combinação infeliz

Uma demonstração com tintas caras
Uma demonstração com tintas caras

O que acontece se eu rolar e mover o mouse ao mesmo tempo? É perfeitamente possível que eu "interaja" inadvertidamente com um elemento quando rolo por ele, acionando uma pintura cara. Isso, por sua vez, pode fazer com que eu ultrapasse meu orçamento de frames de aproximadamente 16,7 ms (o tempo que precisamos ficar abaixo disso para atingir 60 quadros por segundo). Criei uma demonstração para mostrar exatamente o que quero dizer. Ao rolar e mover o mouse, você verá os efeitos de passar o cursor. Mas vamos ver o que o Chrome DevTools faz disso:

DevTools do Chrome mostrando frames caros
DevTools do Chrome mostrando frames caros

Na imagem acima, observe que o DevTools registra o trabalho de pintura quando eu passo o cursor sobre um dos blocos. Eu usei alguns estilos superpesados na minha demonstração para demonstrar essa questão, então estou me esforçando e ocasionalmente usando meu orçamento de frames. A última coisa que quero é fazer essa pintura desnecessariamente, especialmente durante uma rolagem, quando há outro trabalho a ser feito.

Então, como podemos impedir que isso aconteça? Na verdade, a correção é muito simples de implementar. O truque é anexar um gerenciador de scroll que desative os efeitos de passar o cursor e defina um timer para ativá-los novamente. Isso significa que estamos garantindo que, quando você rolar a tela, não precisaremos realizar pinturas de interação caras. Quando você para por tempo suficiente, entendemos que é seguro ligá-los novamente.

O código fica assim:

// Used to track the enabling of hover effects
var enableTimer = 0;

/*
 * Listen for a scroll and use that to remove
 * the possibility of hover effects
 */
window.addEventListener('scroll', function() {
  clearTimeout(enableTimer);
  removeHoverClass();

  // enable after 1 second, choose your own value here!
  enableTimer = setTimeout(addHoverClass, 1000);
}, false);

/**
 * Removes the hover class from the body. Hover styles
 * are reliant on this class being present
 */
function removeHoverClass() {
  document.body.classList.remove('hover');
}

/**
 * Adds the hover class to the body. Hover styles
 * are reliant on this class being present
 */
function addHoverClass() {
  document.body.classList.add('hover');
}

Como podemos notar, usamos uma classe no corpo para rastrear se os efeitos ao passar o cursor são "permitidos" e os estilos subjacentes dependem da presença dessa classe:

/* Expect the hover class to be on the body
 before doing any hover effects */
.hover .block:hover {
 …
}

Isso é tudo!

Conclusão

O desempenho da renderização é fundamental para que os usuários aproveitem seu aplicativo, e você deve sempre procurar manter a carga de trabalho de pintura abaixo de 16 ms. Para fazer isso, faça a integração usando o DevTools durante todo o processo de desenvolvimento para identificar e corrigir gargalos à medida que eles surgirem.

Interações acidentais, especialmente em elementos com pintura, podem ser muito caras e prejudicar o desempenho da renderização. Como você aprendeu, podemos usar um pequeno trecho de código para corrigir isso.

Dê uma olhada em seus sites e aplicativos. Eles poderiam ser suficiente com um pouco de proteção de tinta?