Introdução
O canvas HTML5, que começou como um experimento da Apple, é o padrão mais amplamente aceito para gráficos de modo imediato 2D na Web. Muitos desenvolvedores agora dependem dele para uma ampla variedade de projetos multimídia, visualizações e jogos. No entanto, à medida que os aplicativos que criamos aumentam em complexidade, os desenvolvedores inadvertidamente atingem o limite de desempenho. Há muita sabedoria desconectada sobre como otimizar o desempenho do canvas. Este artigo tem como objetivo 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 gráfica, bem como técnicas específicas de canvas que estão sujeitas a mudanças à medida que as implementações de canvas melhoram. Em particular, à medida que os fornecedores de navegadores implementam a aceleração de GPU de tela, algumas das técnicas de desempenho descritas provavelmente se tornarão menos impactantes. Isso será observado quando apropriado. Este artigo não aborda o uso do canvas HTML5. Para isso, confira estes artigos relacionados ao canvas no HTML5Rocks, este capítulo no site Dive into HTML5 ou o tutorial MDN Canvas.
Teste de desempenho
Para lidar com o mundo em rápida mudança do canvas HTML5, os testes do JSPerf (jsperf.com) verificam se todas as otimizações propostas ainda funcionam. O JSPerf é um aplicativo da Web que permite aos desenvolvedores escrever testes de desempenho de JavaScript. Cada teste se concentra em um resultado que você está tentando alcançar (por exemplo, limpar a tela) e inclui várias abordagens que alcançam o mesmo resultado. O JSPerf 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 JSPerf podem executar o teste no navegador e permitir que o JSPerf armazene os resultados normalizados no Browserscope (browserscope.org). Como as técnicas de otimização neste artigo são respaldadas por um resultado do JSPerf, você pode voltar para ver informações atualizadas sobre se a técnica ainda se aplica ou não. Escrevi 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 baseados na
versão do navegador. Isso acaba sendo uma limitação, já que não sabemos em qual SO
o navegador estava sendo executado ou, o que é ainda mais importante, se o canvas HTML5
teve aceleração de hardware quando o teste de desempenho foi executado. Para saber se o canvas HTML5 do Chrome tem aceleração de hardware, acesse about:gpu na barra de endereço.
Pré-renderizar em uma tela fora da tela
Se você estiver redesenhando primitivos semelhantes na tela em vários frames, como costuma acontecer ao escrever um jogo, é possível melhorar muito o desempenho pré-renderizando grandes partes da cena. A pré-renderização significa usar uma ou mais telas separadas fora da tela para renderizar imagens temporárias 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 frames 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);
}
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 é 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 disso é
a renderização de texto, que é uma operação muito cara.
No entanto, o desempenho ruim do caso de teste "pré-renderizado solto". Ao pré-renderizar, é importante garantir que a tela temporária se ajuste perfeitamente à imagem que você está desenhando. Caso contrário, o ganho de desempenho da renderização fora da tela será compensado pela perda de desempenho ao copiar uma tela grande para outra (que varia em função do tamanho do destino de origem). Uma tela ajustada no teste acima é simplesmente menor:
can2.width = 100;
can2.height = 40;
Em comparação com o tipo flexível, que tem um desempenho pior:
can3.width = 300;
can3.height = 100;
Agrupar chamadas de tela em lote
Como o desenho é uma operação cara, é mais eficiente carregar a máquina de estado de desenho com um longo conjunto de comandos e, em seguida, despejar todos eles 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 desempenho é melhor ao desenhar uma única polilinha:
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 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 as primitivas envolvidas 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 sobre uma máquina de estado que rastreia itens como estilos de preenchimento e traço, além de pontos anteriores que compõem o caminho atual. Ao tentar otimizar o desempenho gráfico, é tentador se concentrar apenas na renderização de gráficos. No entanto, manipular a máquina de estado também pode gerar uma sobrecarga de desempenho. Se você usar várias cores de preenchimento para renderizar uma cena, por exemplo, é mais barato renderizar por cor do que por posicionamento na tela. Para renderizar um padrão de listras, você pode renderizar uma listra, mudar as cores, 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 renderize todas as listras ímpares e, em seguida, todas as listras 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 mudar a máquina de estado é caro.
Renderizar apenas as diferenças de tela, não todo o novo estado
Como é de se esperar, renderizar menos na tela é mais barato do que renderizar mais. Se houver apenas diferenças incrementais entre as novas renderizações, você poderá melhorar significativamente o desempenho desenhando apenas a diferença. Em outras palavras, em vez de limpar a tela inteira antes de desenhar:
context.fillRect(0, 0, canvas.width, canvas.height);
Acompanhe a caixa delimitadora desenhada e limpe apenas ela.
context.fillRect(last.x, last.y, last.width, last.height);
Se você conhece computação gráfica, talvez também conheça essa técnica como "regiões de redesenho", em que a caixa delimitadora renderizada anteriormente é salva e, em seguida, limpa em cada renderização. Essa técnica também se aplica a contextos de renderização baseados em pixels, como ilustrado nesta palestra sobre o emulador de Nintendo em JavaScript.
Usar várias telas em camadas para cenas complexas
Como mencionado antes, desenhar imagens grandes é caro e precisa ser evitado, se possível. Além de usar outra tela para renderização fora da tela, como ilustrado na seção de pré-renderização, também podemos usar telas em camadas umas sobre as outras. Ao usar a transparência na tela de primeiro plano, podemos contar com a GPU para compor os canais alfa no tempo de renderização. Você pode configurar isso da seguinte maneira, com duas telas posicionadas de forma absoluta, uma em cima da 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 em relação a ter apenas uma tela é 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, considere renderizar esses elementos 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 plano, que provavelmente vai ocupar a maior parte da atenção do usuário. Por exemplo, é possível renderizar o primeiro plano sempre que você renderizar, mas renderizar o plano de fundo apenas a cada enésimo frame. Além disso, essa abordagem se generaliza bem para qualquer número de telas compostas se o aplicativo funcionar melhor com esse tipo de estrutura.
Evite shadowBlur
Como muitos outros ambientes gráficos, o canvas 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);
Conhecer várias maneiras de limpar a tela
Como o canvas 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 fundamental para apps Canvas e jogos de canvas HTML5.
Como mencionado na seção Evitar mudanças de estado do canvas,
limpar todo o canvas geralmente não é recomendável, mas, se você precisar fazer
isso, há duas opções: chamar context.clearRect(0, 0, width, height) ou usar um hack específico do canvas para fazer isso:
canvas.width = canvas.width;.No momento em que este artigo foi escrito, clearRect geralmente supera a versão de redefinição de
largura, mas, em alguns casos, usar o hack de redefinição canvas.width
é significativamente mais rápido no Chrome 14.
Tenha cuidado com essa dica, porque ela depende muito da implementação da tela e está sujeita a mudanças. Para mais informações, consulte o artigo de Simon Sarris sobre como limpar a tela.
Evite coordenadas de ponto flutuante
O canvas HTML5 é compatível com a renderização de subpixel, e não há como desativá-la. Se você desenhar com coordenadas que não são números inteiros, ele vai usar automaticamente o anti-aliasing para tentar suavizar as linhas. Confira o efeito visual, retirado deste artigo sobre o desempenho de telas de subpixel de Seb Lee-Delisle (link em inglês):
Se o sprite suavizado não for o efeito que você quer, 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, use várias técnicas inteligentes. A mais eficiente 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;
A análise completa do desempenho está aqui (jsperf).
Esse tipo de otimização não será mais importante quando as implementações de canvas forem aceleradas por GPU, o que permitirá renderizar rapidamente coordenadas não inteiras.
Otimizar as animações com requestAnimationFrame
A API requestAnimationFrame, que é relativamente nova, é a maneira recomendada de
implementar aplicativos interativos no navegador. Em vez de
ordenar que o navegador renderize em uma taxa de tick fixa específica, você
pede que ele chame sua rotina de renderização e seja chamado
quando o navegador estiver disponível. Como um efeito colateral positivo, se a página não estiver
em primeiro plano, o navegador é inteligente o suficiente para não renderizar.
O callback requestAnimationFrame visa uma taxa de callback de 60 QPS, mas
não a garante. Portanto, é necessário acompanhar quanto tempo se passou
desde a última renderização. Por exemplo:
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 à tela e a outras tecnologias de renderização, como o WebGL.
No momento, essa API está disponível apenas no Chrome, Safari e
Firefox. Portanto, use este shim.
A maioria das implementações de tela para dispositivos móveis é lenta
Falar Live sobre dispositivos móveis. Infelizmente, no momento em que este artigo foi escrito, apenas o iOS
5.0 Beta com o Safari 5.1 tinha uma 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 potentes o suficiente para aplicativos modernos baseados em canvas. Vários dos testes do JSPerf descritos acima têm um desempenho
muito pior em dispositivos móveis do que em computadores, o que
restringe bastante os tipos de apps para vários 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 baseados em canvas HTML5 de alto desempenho. Agora que você aprendeu algo novo, otimize suas criações incríveis. Se você não tiver um jogo ou aplicativo para otimizar, confira o Chrome Experiments e o Creative JS para se inspirar.
Referências
- Modo imediato x modo retido.
- Outros artigos sobre telas do HTML5Rocks.
- A seção Canvas do Dive into HTML5.
- O JSPerf permite que os desenvolvedores criem testes de desempenho de JS.
- O Browserscope armazena dados de desempenho do navegador.
- JSPerfView, que renderiza testes do JSPerf como gráficos.
- Postagem do blog de Simon sobre como limpar a tela e o livro dele, HTML5 Unleashed, que inclui capítulos sobre o desempenho da tela.
- Postagem do blog de Sebastian sobre o desempenho de renderização de subpixel.
- Palestra de Ben sobre como otimizar um emulador de NES em JS.
- O novo criador de perfil de tela no Chrome DevTools.