Como melhorar o desempenho da tela HTML5

Introdução

O canvas em HTML5, que começou como um experimento da Apple, é o padrão com mais suporte para gráficos de modo imediato 2D na Web. Muitos desenvolvedores agora contam com ele para uma ampla variedade de projetos multimídia, visualizações e jogos. No entanto, à medida que os aplicativos criados ficam mais complexos, os desenvolvedores acabam atingindo o limite de desempenho sem querer. Existe muito conhecimento sobre otimizar o desempenho da tela. Este artigo visa consolidar parte desse corpo em um recurso mais fácil de entender para desenvolvedores. Este artigo inclui otimizações fundamentais que se aplicam a todos os ambientes de computação, assim como técnicas específicas de canvas que estão sujeitas a mudanças conforme melhoram as implementações de canvas. Em particular, à medida que os fornecedores de navegadores implementam a aceleração de GPU da tela, algumas das técnicas de desempenho discutidas provavelmente terão menos impacto. Isso será observado quando apropriado. Este artigo não aborda o uso do canvas em HTML5. Para isso, consulte estes artigos relacionados à tela no HTML5Rocks, este capítulo do site Dive into HTML5 (link em inglês) ou o tutorial MDN Canvas (link em inglês).

Teste de desempenho

Para lidar com o mundo em constante mudança do canvas do HTML5, os testes da JSPerf (jsperf.com) confirmam se todas as otimizações propostas ainda funcionam. Ele é um aplicativo da Web com o qual os desenvolvedores podem criar testes de desempenho em JavaScript. Cada teste se concentra em um resultado que você está tentando alcançar (por exemplo, limpeza da tela) e inclui várias abordagens que alcançam o mesmo resultado. O Redis erf executa cada abordagem o máximo de vezes possível em um curto período e fornece um número estatisticamente significativo de iterações por segundo. Pontuações mais altas são sempre melhores. Os visitantes de uma página de teste de desempenho do JavaScripterf podem executar o teste no navegador e permitir que o aplicativo JavaScript armazene os resultados normalizados no Browserscope (browserscope.org). Como as técnicas de otimização deste artigo são respaldadas por um resultado do JavaScripterf, você pode voltar para ver informações atualizadas sobre a aplicação ou não da técnica. Criei um pequeno aplicativo auxiliar que renderiza esses resultados como gráficos, incorporados ao longo deste artigo.

Todos os resultados de desempenho neste artigo são codificados na versão do navegador. Isso acabou sendo uma limitação, já que não sabemos em qual SO o navegador estava sendo executado e, o mais importante, se a tela HTML5 foi ou não acelerada por hardware quando o teste de desempenho foi executado. Para descobrir se a tela HTML5 do Chrome é acelerada por hardware, acesse about:gpu na barra de endereço.

Pré-renderizar para uma tela fora da tela

Se você estiver redesenhando primitivas semelhantes na tela em vários frames, como geralmente acontece ao programar um jogo, poderá obter grandes ganhos de desempenho ao pré-renderizar grandes partes da cena. A pré-renderização significa usar telas (ou telas) separadas em que imagens temporárias são renderizadas e, em seguida, renderizar as telas fora da tela de volta para a visível. Por exemplo, suponha que você esteja redesenhando o Mario correndo a 60 quadros por segundo. Você pode redesenhar o chapéu, o bigode e o "M" em cada frame ou pré-renderizar o Mario antes de executar a animação. sem pré-renderização:

// canvas, context are defined
function render() {
  drawMario(context);
  requestAnimationFrame(render);
}

a pré-renderização:

var m_canvas = document.createElement('canvas');
m_canvas.width = 64;
m_canvas.height = 64;
var m_context = m_canvas.getContext('2d');
drawMario(m_context);

function render() {
  context.drawImage(m_canvas, 0, 0);
  requestAnimationFrame(render);
}

Observe o uso de requestAnimationFrame, que será discutido em mais detalhes em uma seção posterior.

Essa técnica é especialmente eficaz quando a operação de renderização (drawMario no exemplo acima) é cara. Um bom exemplo é a renderização de texto, que é uma operação muito cara.

No entanto, o desempenho ruim do caso de teste "pré-renderizado com folga". Durante a pré-renderização, é importante garantir que a tela temporária se encaixe perfeitamente ao redor da imagem que você está desenhando. Caso contrário, o ganho de desempenho da renderização fora da tela será contrabalado pela perda de desempenho da cópia de uma tela grande em outra, que varia de acordo com o tamanho do destino de origem. Uma tela confortável no teste acima é simplesmente menor:

can2.width = 100;
can2.height = 40;

Em comparação com o criativo flexível que produz um desempenho inferior:

can3.width = 300;
can3.height = 100;

Chamadas de canvas em lote

Como o desenho é uma operação cara, é mais eficiente carregar a máquina de estado de desenho com um conjunto longo de comandos e, em seguida, fazer com que ela despeje todos no buffer de vídeo.

Por exemplo, ao desenhar várias linhas, é mais eficiente criar um caminho com todas as linhas e desenhá-lo com uma única chamada de desenho. Em outras palavras, em vez de desenhar linhas separadas:

for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.beginPath();
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
  context.stroke();
}

O desenho de uma única polilinha tem um desempenho melhor:

context.beginPath();
for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
}
context.stroke();

Isso também se aplica ao mundo do canvas em HTML5. Ao desenhar um caminho complexo, por exemplo, é melhor colocar todos os pontos no caminho em vez de renderizar os segmentos separadamente (jsperf).

No entanto, com o Canvas, há uma exceção importante a essa regra: se os primitivos envolvidos no desenho do objeto desejado tiverem caixas delimitadoras pequenas (por exemplo, linhas horizontais e verticais), talvez seja mais eficiente renderizá-las separadamente (jsperf).

Evitar mudanças desnecessárias no estado da tela

O elemento de tela HTML5 é implementado em uma máquina de estado que rastreia elementos como estilos de traço e preenchimento, bem como pontos anteriores que compõem o caminho atual. Ao tentar otimizar a performance gráfica, é tentador se concentrar apenas na renderização gráfica. No entanto, manipular a máquina de estado também pode gerar uma sobrecarga de desempenho. Se você usa várias cores de preenchimento para renderizar uma cena, por exemplo, é mais barato renderizar por cor do que por posicionamento na tela. Para renderizar uma estampa de listras finas, é possível renderizar uma listra, mudar de cor, renderizar a próxima listra etc:

for (var i = 0; i < STRIPES; i++) {
  context.fillStyle = (i % 2 ? COLOR1 : COLOR2);
  context.fillRect(i * GAP, 0, GAP, 480);
}

Ou então, renderize todas as listras ímpares e depois todas as pares:

context.fillStyle = COLOR1;
for (var i = 0; i < STRIPES/2; i++) {
  context.fillRect((i*2) * GAP, 0, GAP, 480);
}
context.fillStyle = COLOR2;
for (var i = 0; i < STRIPES/2; i++) {
  context.fillRect((i*2+1) * GAP, 0, GAP, 480);
}

Como esperado, a abordagem entrelaçada é mais lenta porque a alteração da máquina de estado é cara.

Renderizar apenas as diferenças da tela, não todo o novo estado

Como seria de se esperar, renderizar menos na tela é mais barato do que renderizar mais. Se você tiver apenas diferenças incrementais entre redesenhos, poderá ter um aumento significativo no desempenho apenas desenhando a diferença. Em outras palavras, em vez de limpar toda a tela antes de desenhar:

context.fillRect(0, 0, canvas.width, canvas.height);

Rastreie e limpe apenas a caixa delimitadora desenhada.

context.fillRect(last.x, last.y, last.width, last.height);

Se você tem familiaridade com computação gráfica, talvez conheça essa técnica como "regiões de redesenho", em que a caixa delimitadora renderizada anteriormente é salva e depois apagada em cada renderização. Essa técnica também se aplica a contextos de renderização baseados em pixels, conforme ilustrado por esta palestra em JavaScript do emulador Nintendo (link em inglês).

Usar telas em várias camadas para cenas complexas

Como mencionado anteriormente, o desenho de imagens grandes é caro e precisa ser evitado se possível. Além de usar outra tela para renderizar fora da tela, conforme ilustrado na seção de pré-renderização, também é possível usar telas sobrepostas umas sobre as outras. Ao usar a transparência na tela em primeiro plano, podemos confiar na GPU para compor os Alfas juntos no momento da renderização. Faça essa configuração da seguinte maneira, com duas telas absolutamente posicionadas uma sobre a outra.

<canvas id="bg" width="640" height="480" style="position: absolute; z-index: 0">
</canvas>
<canvas id="fg" width="640" height="480" style="position: absolute; z-index: 1">
</canvas>

A vantagem sobre ter apenas uma tela aqui é que, quando desenhamos ou limpamos a tela de primeiro plano, nunca modificamos o plano de fundo. Se o jogo ou app multimídia puder ser dividido em primeiro e segundo plano, renderize-os em telas separadas para melhorar significativamente o desempenho.

Muitas vezes, é possível aproveitar a percepção humana imperfeita e renderizar o plano de fundo apenas uma vez ou em uma velocidade mais lenta em comparação com o primeiro, que provavelmente ocupará a maior parte da atenção do usuário. Por exemplo, é possível renderizar o primeiro plano sempre que você renderizar, mas o segundo plano somente a cada N frame. Observe também que essa abordagem generaliza bem para qualquer número de telas compostas se seu aplicativo funcionar melhor com esse tipo de estrutura.

Evitar o shadowBlur

Como muitos outros ambientes gráficos, o canvas em HTML5 permite que os desenvolvedores desfoquem primitivos, mas essa operação pode ser muito cara:

context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = 'rgba(255, 0, 0, 0.5)';
context.fillRect(20, 20, 150, 100);

Conheça várias maneiras de limpar o canvas

Como a tela HTML5 é um paradigma de desenho de modo imediato, a cena precisa ser redesenhada explicitamente em cada frame. Por isso, limpar o canvas é uma operação fundamentalmente importante para apps e jogos de canvas em HTML5. Como mencionado na seção Evitar mudanças de estado da tela, limpar toda a tela geralmente não é desejável, mas se você precisa fazer isso, há duas opções: chamar context.clearRect(0, 0, width, height) ou usar uma hack específica da tela para fazer isso: canvas.width = canvas.width. No momento, clearRect geralmente supera a versão de redefinição de largura, mas, em alguns casos, usar a hack de redefinição canvas.width é significativamente mais rápida no Chrome 14.

Cuidado com essa dica, já que ela depende muito da implementação da tela subjacente e está muito sujeita a mudanças. Para mais informações, consulte o artigo de Simon Sarris sobre limpeza da tela.

Evitar coordenadas de ponto flutuante

A tela HTML5 oferece suporte à renderização de subpixels, e não há como desativá-la. Se você desenhar com coordenadas que não sejam números inteiros, ele usará automaticamente o anti-aliasing para tentar suavizar as linhas. Veja o efeito visual retirado deste artigo sobre desempenho da tela de subpixels por Seb Lee-Delisle (em inglês):

Subpixel

Se o sprite suavizado não for o efeito que você procura, pode ser muito mais rápido converter as coordenadas em números inteiros usando Math.floor ou Math.round (jsperf):

Para converter as coordenadas de ponto flutuante em números inteiros, é possível usar várias técnicas inteligentes. A mais eficiente delas envolve adicionar metade ao número de destino e, em seguida, realizar operações bit a bit no resultado para eliminar a parte fracionária.

// With a bitwise or.
rounded = (0.5 + somenum) | 0;
// A double bitwise not.
rounded = ~~ (0.5 + somenum);
// Finally, a left bitwise shift.
rounded = (0.5 + somenum) << 0;

O detalhamento completo do desempenho está aqui (jsperf).

Esse tipo de otimização não importa mais quando as implementações de tela são aceleradas pela GPU, o que permite renderizar coordenadas não inteiras rapidamente.

Otimizar suas animações com requestAnimationFrame

A API requestAnimationFrame relativamente nova é a maneira recomendada de implementar aplicativos interativos no navegador. Em vez de comandar o navegador para renderizar a uma taxa de marcação fixa específica, você pede educadamente que ele chame sua rotina de renderização e seja chamado quando o navegador estiver disponível. Um bom efeito colateral: se a página não está em primeiro plano, o navegador é inteligente o suficiente para não renderizar. O callback requestAnimationFrame tem como objetivo uma taxa de 60 QPS, mas não garante isso. Portanto, você precisa acompanhar quanto tempo passou desde a última renderização. O resultado será semelhante a este:

var x = 100;
var y = 100;
var lastRender = Date.now();
function render() {
  var delta = Date.now() - lastRender;
  x += delta;
  y += delta;
  context.fillRect(x, y, W, H);
  requestAnimationFrame(render);
}
render();

Esse uso de requestAnimationFrame se aplica ao canvas e a outras tecnologias de renderização, como WebGL. No momento em que este artigo foi escrito, essa API só estava disponível no Chrome, Safari e Firefox. Portanto, use este paliativo.

A maioria das implementações de canvas para dispositivos móveis é lenta

Vamos falar sobre dispositivos móveis. No momento, apenas o iOS 5.0 Beta que executa o Safari 5.1 tinha implementação de tela para dispositivos móveis acelerada por GPU. Sem a aceleração de GPU, os navegadores para dispositivos móveis geralmente não têm CPUs com capacidade suficiente para aplicativos modernos baseados em tela. Vários testes de JavaScript de resposta a problemas descritos acima têm uma ordem de magnitude pior em dispositivos móveis em comparação com computadores, o que restringe bastante os tipos de apps entre dispositivos que podem ser executados.

Conclusão

Para recapitular, este artigo abordou um conjunto abrangente de técnicas de otimização úteis que ajudarão você a desenvolver projetos de alto desempenho com base em telas HTML5. Agora que você aprendeu algo novo aqui, vá em frente e otimize suas criações incríveis. Ou, se você ainda não tiver um jogo ou aplicativo para otimizar, confira os experimentos do Chrome e o Creative JS para se inspirar.

Referências