Nos bastidores: Bubbles

Em uma postagem anterior, discutimos sobre os aprimoramentos no desempenho de JavaScript introduzidos no IE10. Hoje publicamos o Bubbles, originalmente inspirado pela simulação de Alex Gavriolov BubbleMark, para explorar alguns desses avanços. A versão atual foi bastante expandida para aproveitar os novos recursos da plataforma da Web e inclui características comuns de jogos HTML5. Nesta postagem, espiamos por trás dos bastidores para ver como a demonstração é construída e examinamos os principais fatores que afetam seu desempenho.

Captura de tela da demonstração do Bubbles executado no IE10 estilo Metro do Windows 8 Release Preview
Demonstração do Bubbles executado no IE10 estilo Metro do Windows 8 Release Preview

A estrutura da demonstração

A demonstração consiste em várias bolhas animadas flutuando pelo espaço. Na base, existe um mecanismo físico JavaScript relativamente simples. Em cada quadro de animação, aproximadamente 60 vezes por segundo, o mecanismo físico recalcula as posições de todas as bolhas, ajusta a velocidade de cada uma aplicando gravidade e calcula colisões. Todos esses cálculos envolvem muitas contas de ponto flutuante. Cada bolha é representada na tela por um elemento de imagem DOM ao qual é aplicado uma transformação CSS. A imagem é traduzida primeiro quanto à sua origem e depois é ajustada dinamicamente para produzir o efeito da bolha enchendo. Em JavaScript, cada bolha é representada como um objeto com propriedades de acessador, conforme introduzido no ECMAScript 5.

Por trás das bolhas flutuantes existe uma grande imagem, que começa oclusa por uma máscara totalmente opaca renderizada em um elemento <canvas>. Sempre que duas bolhas colidem, uma parte da máscara de oclusão é removida aplicando um filtro Gaussiano ao componente de opacidade da máscara para produzir um efeito de transparência difuso. Esse processo também envolve várias multiplicações de ponto flutuante, que são executadas em elementos de matrizes tipadas, se houver suporte no navegador.

Sobre as bolhas flutuantes há uma superfície de toque, que responde à entrada de toque, se houver suporte no navegador executado, ou a eventos de mouse, se não houver. Em resposta ao toque, uma repulsão magnética simulada é aplicada às bolhas flutuantes espalhando-as.

Animação eficiente

No IE10, introduzimos o suporte à API requestAnimationFrame. Ao compilar aplicativos JavaScript com animação, continuamos a recomendar o uso dessa API (em vez de setTimeout ou setInterval) em navegadores que dão suporte a ela. Como vimos em uma postagem anterior que explorava como aproveitar ao máximo o seu hardware, essa API permite que se obtenha a taxa máxima de quadros com suporte na tela disponível e se evite o trabalho excessivo que permaneceria invisível ao usuário. Executando o mínimo de operações para fornecer a melhor experiência do usuário, é possível economizar energia da bateria. O IE10 Release Preview dá suporte a essa API sem prefixo de fornecedor, mas a versão prefixada também é mantida para fornecer compatibilidade com IE10 Preview Releases anteriores. A demonstração do Bubbles usa essa API e recorre a setTimeout quando ela não está disponível.

Demo.prototype.requestAnimationFrame = function () {

var that = this;

if (window.requestAnimationFrame)

this.animationFrameTimer =

window.requestAnimationFrame(function () { that.onAnimationFrame(); });

else

this.animationFrameTimer =

setTimeout(function () { that.onAnimationFrame(); }, this.animationFrameDuration);

}

 

Demo.prototype.cancelAnimationFrame = function () {

if (window.cancelRequestAnimationFrame)

window.cancelRequestAnimationFrame(this.animationFrameTimer);

else

clearTimeout(this.animationFrameTimer);

}

Convertendo valores DOM de cadeias em números

JavaScript é muito flexível e inclui uma série de conversões automáticas entre valores de tipos diferentes. Por exemplo, os valores de cadeia são automaticamente convertidos em valores de número quando usados em operações aritméticas. Nos navegadores modernos, essa conveniência pode ter um custo de desempenho surpreendentemente alto. Digitar código especializado emitido por compiladores JavaScript de última geração é muito eficiente em aritmética para valores de tipos conhecidos, mas gera uma sobrecarga muito alta quando encontra valores de tipos inesperados.

Quando a demonstração do Bubbles é carregada, a propriedade numberOfBubbles começa com um valor de 100. Em cada quadro de animação, a posição de cada bolha é ajustada:

function Demo() {

this.numberOfBubbles = 100;

//...

}

 

Demo.prototype.moveBubbles = function(elapsedTime) {

for (var i = 0; i < this.numberOfBubbles; i++) {

this.bubbles[i].move(elapsedTime, this.gravity);

}

}

Quando o usuário seleciona um valor diferente na interface do usuário, o valor da propriedade numberOfBubbles deve ser ajustado de acordo. Um manipulador de eventos simplificado pode fazer isso desta forma:

Demo.prototype.onNumberOfBubblesChange = function () {

this.numberOfBubbles = document.getElementById("numberOfBubblesSelector").value;

//...

}

Essa maneira aparentemente natural de ler a entrada do usuário gera cerca de 10% de sobrecarga na parte JavaScript da demonstração. Como o valor obtido da lista suspensa e atribuído a numberOfBubbles é uma cadeia de caracteres (e não um número), ele tem de ser convertido em um número a cada iteração do loop moveBubbles para cada quadro da animação.

Isso demonstra que é uma boa prática converter explicitamente os valores extraídos do DOM em números antes de usá-los em operações aritméticas. Os valores de propriedades DOM são normalmente de cadeia de tipos em JavaScript e a cadeia automática para conversões de número pode ser bastante dispendiosa quando feita repetidamente. Um jeito melhor de atualizar numberOfBubbles com base na seleção do usuário, como vemos na demonstração, é o seguinte:

Demo.prototype.onNumberOfBubblesChange = function () {

this.numberOfBubbles = parseInt(document.getElementById("numberOfBubblesSelector").value);

//...

}

Trabalhando com propriedades de acessador ES5

As propriedades de acessador ECMAScript 5 são um mecanismo conveniente para encapsulamento de dados, propriedades computadas, validação de dados ou notificação de alterações. Na demonstração do Bubbles, sempre que uma bolha enche, o raio é ajustado, o que define a propriedade radiusChanged computada para indicar que a imagem da bolha precisa ser redimensionada.

Object.defineProperties(Bubble.prototype, {

//...

radius: {

get: function () {

return this.mRadius;

},

set: function (value) {

if (this.mRadius != value) {

this.mRadius = value;

this.mRadiusChanged = true;

}

}

},

//...

});

Em todos os navegadores, as propriedades do acessador adicionam sobrecarga em comparação com propriedades de dados. A quantidade precisa de sobrecarga varia conforme o navegador.

Minimizando o acesso da Tela ImageData

É uma prática já estabelecida minimizar o número de chamadas ao DOM em loops no caminho de desempenho crítico. Por exemplo, se a demonstração do Bubbles atualizasse o local de cada bolha consultando o elemento correspondente no documento (conforme abaixo), o desempenho seria afetado negativamente.

Bubble.prototype.render = function () {

document.getElementById("bubble" + this.id).style.left = Math.round(this.x) + "px";

document.getElementById("bubble" + this.id).style.top = Math.round(this.y) + "px";

this.updateScale();

}

Em vez disso, o elemento correspondente a cada bolha é armazenado em cache no objeto de bolha em JavaScript uma vez e depois acessado diretamente em cada quadro de animação.

Bubble.prototype.render = function () {

this.element.style.left = Math.round(this.x) + "px";

this.element.style.top = Math.round(this.y) + "px";

this.updateScale();

}

O que talvez não esteja tão claro é que é preciso evitar esse tipo de sobrecarga ao trabalhar com <canvas>. O objeto obtido chamando canvas.getContext("2D").getImageData() também é um objeto DOM. O código abaixo pode ser usado na demonstração para traçar o efeito de colisão da bolha com a tela. Nessa versão, imgData.data é lido a cada iteração do loop, o que requer uma chamada ao DOM e adiciona uma grande sobrecarga.

BubbleTank.prototype.renderCollisionEffectToCanvas = function(px, py) {

var imgData = this.canvasContext.getImageData(/*...*/)

//...

for (var my = myMin; my <= myMax; my++) {

for (var mx = mxMin; mx <= mxMax; mx++) {

var i = (mx + gaussianMaskRadius) + (my + gaussianMaskRadius) * gaussianMaskSize;

imgData.data[4 * i + 3] = 255 * occlusionMask[(px + mx) + (py + my) * canvasWidth];

}

}

this.canvasContext.putImageData(imgData, px - gaussianMaskRadius, py - gaussianMaskRadius);

}

Uma maneira melhor de atualizar os dados da imagem <canvas> é armazenar em cache a propriedade data conforme no seguinte trecho de código. A propriedade data é uma matriz tipada (PixelArray) e pode ser acessada com muita eficiência com JavaScript.

BubbleTank.prototype.renderCollisionEffectToCanvas = function(px, py) {

var imgData = this.canvasContext.getImageData(/*...*/)

var imgColorComponents = imgData.data;

//...

for (var my = myMin; my <= myMax; my++) {

for (var mx = mxMin; mx <= mxMax; mx++) {

var i = (mx + gaussianMaskRadius) + (my + gaussianMaskRadius) * gaussianMaskSize;

imgColorComponents[4 * i + 3] =

255 * occlusionMask[(px + mx) + (py + my) * canvasWidth];

}

}

this.canvasContext.putImageData(imgData, px - gaussianMaskRadius, py - gaussianMaskRadius);

}

Usando matrizes tipadas para armazenar números de ponto flutuante

No IE10, adicionamos suporte a matrizes tipadas. Ao trabalhar com números de ponto flutuante, convém usar matrizes tipadas (Float32Array ou Float64Array), em vez de matrizes JavaScript (Array). As matrizes JavaScript podem conter elementos de qualquer tipo, mas normalmente requerem que os valores de ponto flutuante sejam alocados no heap (boxing) antes de serem adicionados à matriz. Isso afeta o desempenho. Para obter um alto desempenho consistente nos navegadores modernos, use Float32Array ou Float64Array para indicar a intenção de armazenar valores de ponto flutuante. Com isso, você ajuda o mecanismo JavaScript a evitar o boxing de heap e ativar outras otimizações do compilador, como gerar operações especializadas de tipo.

BubbleTank.prototype.generateOcclusionMask = function() {

if (typeof Float64Array != "undefined") {

this.occlusionMask = new Float64Array(this.canvasWidth * this.canvasHeight);

} else {

this.occlusionMask = new Array(this.canvasWidth * this.canvasHeight);

}

this.resetOcclusionMask();

}

O exemplo acima revela como a demonstração do Bubbles usa Float64Arrays para manter e atualizar a máscara de oclusão aplicada à tela que oculta a imagem de plano de fundo. Se o navegador não der suporte a matrizes tipadas, o código recorrerá a matrizes normais. A vantagem de usar matrizes tipadas na demonstração do Bubbles varia conforme as configurações. Em uma janela de tamanho médio do IE10, as matrizes tipadas aumentam a taxa de quadros geral em cerca de 10%.

Resumo

Nesta postagem do blog, exploramos a demonstração do Bubbles publicada recentemente e examinamos como ele obtém ganhos drásticos na execução em JavaScript do IE10 Release Preview. Compartilhamos algumas técnicas importantes para obter um bom desempenho em aplicativos baseados em animação. Para obter mais detalhes técnicos sobre as alterações no tempo de execução de JavaScript (Chakra) do IE10 Release Preview, consulte a postagem anterior. Estamos felizes com a grande melhora de desempenho e os novos recursos do IE10 Release Preview e esperamos que eles permitam que aplicativos ainda mais fascinantes sejam criados usando os padrões e as tecnologias da Web.

—Andrew Miadowicz, gerente de programas, JavaScript