GPU e o Browser

GPU e o Browser

Nós, developers, temos tendência a ficar obececados (e bem) com a performance do software que escrevemos. O problema é quando essa obcessão é mal direcionada e nos preocupamos mais com as ferramentas que usamos do que com tudo o resto.

Um bom exemplo disto é o desenvolvimento front-end para a web. Quem é que nunca ouviu “Hey vocês ainda não usam React? É muito mais rápido! Usa uma cena chamada DOM virtual que é muito mais rápido!” seguido de “Oh isso é old news, nós já estamos a migrar para Mercury, que nos benchmarks é 5x mais rápido que isso…“?

Estas histórias multiplicam-se por muitas áreas, não é exclusivo da web. Outro exemplo clássico é sacrificar a clareza do código em prol da performance:

int element;
for(int index = 0; index < something; i++) {
  element = stuff[index];
}

Em vez de:

for(int index = 0; index < something; i++) {
  int element = stuff[index];
}

Mesmo que o segundo caso seja mais conciso, menos propício a erros (a variável element está limitada no scope ao estritamente necessário) e tenha exatamente o mesmo desempenho na esmagadora maioria dos casos, porque os compiladores são desenvolvidos o suficiente para otimizar casos tão triviais.

Podia continuar a dar exemplos, mas já chega de desabafar. O que queria mostrar hoje é uma técnica extremamente simples mas que pode ter um impacto enorme na performance de um cliente numa página web.

Sem querer entrar em muito detalhe sobre o ciclo de render de um browser (ou seja, como reage a alterações no DOM, podem ler mais sobre isso aqui), o importante é saber que os browsers constroem (recorrendo à DOM Tree e à CSS Tree) uma coisa chamada Render Tree, que contém Render Objects que por sua vez estão organizados em RenderLayers que são desenhados sobre Graphical Layers (detalhes).

Render Tree

Confuso? Não é muito relevante, disto tudo só interessa retirar que podemos aumentar a performance evitando que uma porção grande da página tenha que ser re-desenhada (é esta a lógica da nova onde de frameworks front-end que usam o render engine do React ou o virtual-dom). O ideal, em termos de velocidade, era dar a cada Render Object o seu próprio Graphics/Composite Layer, mas isso não é feito por razões de poupança de VRAM (memória da placa gráfica).

Podemos ver tudo isto a acontecer neste exemplo. Antes de usarem, tenham cuidado com o número de imagens que usam (podem modificar no Javascript), porque vos pode bloquear a tab. Para verem o que se está a passar devem também ligar o contador de FPS do brower e fazer com que mostre as várias layers (como fazer isso). Depois:

  1. Carregar no ícone para começar a animação. Nada fluído, certo?
  2. Descomentem o transform em image e carreguem em ‘Run’. Vêm que apareceu um quadrado à volta do ícone? Isso é um Graphical Layer novo. Notem que o uso da memória da GPU também disparou, isto é porque todas as texturas são carregadas para lá, em vez de ficarem na RAM para serem usadas pelo CPU. Experimentem carregar no ícone e vejam como está muito mais fluído, com FPSs muito superiores.
  3. Voltem a comentar o que descomentaram, e desta vez descomentem as animações em .image.animated. Vêm que o novo layer só aparece quando carregam no ícone, enquanto com o método anterior estava sempre lá? A razão para isso é que, com transformações 2D, esse layer só é criado on-the-fly.

Há outro tipo de operações que forçam a criaçam deste layer à parte:

  • Elementos <video> e <canvas>
  • Filtros CSS
  • Elementos que sobrepõe (via z-index) a algum que esteja num composite layer próprio
  • Transformações 3D
  • etc

No fundo, é um trade-off entre velocidade e memória. Outro problema que pode surgir é com o aspeto do tipo de letra, já que a GPU e o CPU não o fazem da mesma forma. Um truque engraçado para forçar a que um elemento seja renderizado na GPU é acrescentar-lhe, como fizemos no exemplo, algo como transform: translateZ(0).

comments powered by Disqus