Como evitar tintas desnecessárias

Introdução

A pintura dos elementos de um site ou aplicativo pode ser muito cara e ter um efeito negativo no desempenho do tempo de execução. Neste artigo, vamos analisar rapidamente o que pode acionar a pintura no navegador e como impedir que pinturas desnecessárias ocorram.

Pintura: um tour super-rápido

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

O processo de pintura em si é interessante. No Chrome, essa árvore combinada de DOM e CSS é rasterizada por um software chamado Skia. Se você já usou a API Skia do elemento canvas, ela vai parecer muito familiar. Há muitas funções no estilo moveTo e lineTo, além de várias mais avançadas. Basicamente, 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, que ajuda a combiná-los para gerar a imagem final na tela.

Dom para pixels

A carga de trabalho do Skia é diretamente afetada pelos estilos que você aplica aos elementos. Se você usar estilos logicamente pesados, o Skia terá mais trabalho. Colt McAnlis escreveu um artigo sobre como o CSS afeta o peso da renderização da página. Leia para saber mais.

Com tudo isso, a pintura leva tempo para ser executada. Se não reduzirmos, vamos ultrapassar o orçamento de frames de aproximadamente 16 ms. Os usuários vão notar que perdemos frames e vão considerar isso como um problema, o que prejudica a experiência do usuário no app. Não queremos isso, então vamos ver que tipo de coisas exigem a pintura e o que podemos fazer a respeito.

Rolagem

Sempre que você rola para cima ou para baixo no navegador, ele precisa repintar o conteúdo antes que ele apareça na tela. Se tudo correr bem, essa será apenas uma pequena área, mas, mesmo que seja o caso, os elementos que precisam ser desenhados podem ter estilos complexos aplicados. Só porque você tem uma área pequena para pintar não significa que o processo será rápido.

Para saber quais áreas estão sendo pintadas novamente, use o recurso "Mostrar retângulos de pintura" no Chrome DevTools. Basta clicar na pequena engrenagem no canto inferior direito. Em seguida, com o DevTools aberto, basta interagir com a página para ver retângulos piscando mostrando 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 da rolagem é fundamental para o sucesso do seu site. Os usuários realmente percebem quando seu site ou aplicativo não rola bem e não gostam disso. Portanto, temos um interesse pessoal em manter o trabalho de pintura leve durante a rolagem para que os usuários não vejam o travamento.

Eu já escrevi um artigo sobre a performance de rolagem. Leia-o para saber mais sobre os detalhes da performance 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, passar o cursor, o Chrome terá que repintar o elemento afetado. E, assim como na rolagem, se uma pintura grande e complexa for necessária, a taxa de frames vai cair.

Todo mundo quer animações de interação legais e suaves. Então, vamos verificar se os estilos que mudam na 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 a tela e mover o mouse ao mesmo tempo? É perfeitamente possível que eu inadvertentemente "interaja" com um elemento ao rolar para baixo, acionando uma pintura cara. Isso, por sua vez, poderia ultrapassar meu orçamento de frames de cerca de 16,7 ms (o tempo que precisamos ficar abaixo disso para atingir 60 frames por segundo). Criei uma demonstração para mostrar exatamente o que quero dizer. Esperamos que os efeitos de passar o cursor sejam exibidos ao rolar e mover o mouse, mas vamos conferir o que o DevTools do Chrome faz disso:

As DevTools do Chrome mostram frames caros
As DevTools do Chrome mostram frames caros

Na imagem acima, é possível ver que as Ferramentas do desenvolvedor estão registrando o trabalho de pintura quando passo o cursor sobre um dos blocos. Para mostrar o ponto, usei alguns estilos super pesados na minha demonstração, e estou chegando no meu orçamento de frames e, às vezes, ultrapassando-o. A última coisa que eu quero é ter que fazer esse trabalho de pintura desnecessariamente, especialmente durante um rolagem quando há outro trabalho a ser feito.

Então, como podemos impedir que isso aconteça? A correção é bastante simples de implementar. O truque aqui é anexar um gerenciador scroll que desativará os efeitos de passar o cursor e definirá um timer para ativá-los novamente. Isso significa que garantimos que, ao rolar, não será necessário realizar pinturas de interação caras. Quando você para por tempo suficiente, consideramos que é seguro reativá-los.

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 você pode ver, usamos uma classe no corpo para rastrear se os efeitos de passar o cursor são "permitidos" ou não, 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 {
 
}

Pronto.

Conclusão

O desempenho de renderização é essencial para que os usuários aproveitem o app. Por isso, sempre tente manter a carga de trabalho de pintura abaixo de 16 ms. Para isso, integre o uso do DevTools ao longo do processo de desenvolvimento para identificar e corrigir gargalos à medida que eles surgem.

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

Analise seus sites e aplicativos. Eles precisam de um pouco de proteção?