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).
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:
- Carregar no ícone para começar a animação. Nada fluído, certo?
- Descomentem o
transform
emimage
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. - 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)
.