Na hora de desenvolver um projeto em Javascript, é importante ter em mente o objetivo do mesmo, o foco e a forma que ele deverá se comportar no futuro.

Quando falamos de alto desempenho em Javascript, não estamos apenas falando de trabalhar com bons algoritmos, devido às possibilidades diferentes de implementação que a linguagem permite, também estamos falando de padrões de código.

Uma ferramenta muito útil nos dias de hoje é o jsPerf que permite fazer benchmarks (testes de desempenho) com diferentes implementações e browsers. A ferramenta é incompatível com alguns browsers, então se você está trabalhando com um dispositivo móvel ou uma compilação antiga do webkit, é provável que você não consiga usar todos recursos da ferramenta.

Navegando pela lista de testes do jsPerf é possível achar vários testes interessantes. E mesmo assim sempre me deparo com testes terrivelmente mal construídos, acontece por descuido, mas também por falta de conhecimento de como utilizar a ferramenta, pode-se até afirmar que a maioria dos testes estão quebrados e foi isso que me motivou a escrever este artigo. Quero ajudar aqueles pretendem extrair o melhor de seus códigos e aprender com as possibilidades que o Javascript lhes propicia.

Qual o melhor algoritmo afinal?

É uma questão muito difícil de responder, vai depender do browser, do motor que vai processar, pré-processar, pré-otimizar/pré-compilar antes de processar seu código e por fim ainda dependerá do hardware utilizado e em alguns casos mais raros, da forma em que este prioriza I/O (Interfaces humanas, bases de dados, DOM, temporizadores, Workers, XHR e por ai vai).

Aí vem a pergunta, se é tão complicado avaliar, então tanto faz? Não!

Na hora de escolher seu algoritmo tenha foco. Se um browser tiver muito desempenho em determinada opção e outros podem até ser piores, porém são resultados aceitáveis, opte pela opção do browser que te deu melhor desempenho. É possível, e provável, que com a corrida dos browsers os outros venham aprimorar seus motores para igualar-se.

Quando os browsers destoarem muito os resultados, escolha a média, sempre tendendo aos motores mais novos.

Quando teste for inconclusivo

Neste caso, escolha a melhor escrita, evite escrever códigos muito complexos para ganhar apenas um pouquinho de desempenho, quando uma escrita mais legível poderia ser suficiente para um desempenho aceitável.

Criando testes

Vamos falar dos campos do jsPerf. E logo quando você entra no site, este lhe pede nome, email e url. É opcional, mas é uma ótima maneira de encontrar novamente seus testes.

Os testes dos usuários ficam em http://jsperf.com/browse/usuario onde usuário é seu nome, removendo caracteres não alfanumérico e substituindo espaço por hífen. No meu caso (Gabriel R. Giannattasio): http://jsperf.com/browse/gabriel-r-giannattasio

Mas nada mais importante que um bom titulo e descrição para que você e outros desenvolvedores possam se beneficiar dos seus testes.

Preparando código

Existe diferença entre o campo para HTML e o campo para configuração (setup) dos testes.

jsPerf: JavaScript performance playgroundNo primeiro campo é o local onde você deve colocar seu HTML, neste campo você deve preparar o DOM para durante os testes ser validado, inspecionado e alterado (quando alterar, use corretamente os campos setup e teardown, falaremos deles mais tarde), também é um ótimo (e indicado) local pra você carregar bibliotecas como jQuery, Prototype, entre outras, como indica nos botões que irão lhe auxiliar nessa tarefa. Os outros campos não são indicados pra isso.

Se você criou sua própria biblioteca, use o campo HTML para inseri-la.

Usarei como exemplo o underscore.js, que tem seu código fonte hospedado no github. 

underscore-min-raw

Para isso vou usar o arquivo underscore-min.js, que neste caso já é minificado e o mais indicado para testes. Com o link do raw, é simples colar este na área do HTML, veja o seguinte código:

 HTML |  copiar código |? 
  1. <script src="//raw.github.com/documentcloud/underscore/master/underscore-min.js"></script>

Nota-se que não tem “http” ou “https” no inicio, apenas “//”, se o servidor oferecer o arquivo pelos 2 protocolos, você não vai precisar especificar.

Posso colar o código da minha biblioteca dentro do tag script?

Pode, mas não é legal. Fica ilegível, você não fornece a fonte de origem do código, em pouco tempo é provável que se torne um teste obsoleto.

Posso preparar meu código de teste dentro de uma tag script?

Não, olhe este teste e sua revisão. Na revisão o código que prepara os testes foi movido de dentro do tag SCRIPT no campo HTML para campo setup de forma correta. No próprio comentário Mathias Bynens explica que “isto evita pesquisa de escopo pela variável, então os testes que usam estas variáveis não são penalizados. Tornando o teste mais preciso”.

Então eu devo colocar minha biblioteca no campo “setup”?

Leve em consideração que isso vai encher o escopo do seu teste com as variáveis que sua biblioteca expõe, deixará o campo cheio de código (boa parte apenas poluindo seu ambiente de teste). Fazendo como no exemplo anterior e após o carregamento no setup referenciando apenas o que você irá de fato usar no seu teste, você terá um teste limpo e bem direcionado ao seu objetivo.

O que colocar no campo “setup”?

Como já explicado, o campo setup vai construir o escopo de execução do seu teste. Então se o seu foco é testar uma operação de comparação de variáveis por exemplo, as variáveis devem estar declaradas no campo setup e nos testes apenas devem existir os códigos que deverão ser comparados.

Neste teste, por exemplo, coloquei a implementação dos algoritmos (minha biblioteca) no campo HTML dentro do tag SCRIPT. No pré-teste (campo setup) clonei as arrays que foram preparadas de forma aleatórias no meu campo HTML, para todas as arrays testadas sejam iguais durante os testes. Na segunda revisão do teste resolvi ver qual dos algoritmos era mais rápido no geral (realizando de pequenas a grandes operações) e neste caso o resultado mudou, onde o mySort é expressivamente melhor em arrays pequenas, o newSort em média quando temos arrays grandes, se torna mais eficiente.

O que fazer com campo teardown?

Se você já fez pesquisas com jsPerf, deve ter reparado que o teardown é muito pouco, quase nada utilizado. Basicamente ele será executado logo após o seu teste (pós-teste) e não vai influenciar no tempo do seu teste.

O teardown poderia redefinir suas variáveis para o valor original, mas isso é mais indicado para ser feito no campo setup.

Este recurso é mais utilizado quando você abre uma conexão e quer garantir que esta foi fechada após seu teste. Por exemplo, quando testando XHR, eventos assíncronos, API externa que necessita de algum pós comando, esses tipos de coisas. Logo falaremos mais de testes assíncronos.

Campo preparation code, cadê?

Se você ouviu falar, ou viu em versões antigas o campo “Preparation code”, bom, ele não existe mais por uma boa causa: o campo HTML é um ótimo lugar pra você preparar o código da sua execução, era um campo redundante, onde ele apenas adicionava um tag SCRIPT ao campo HTML com conteúdo que você colocou.

Manipulando DOM

Não esqueça que quando for manipular o DOM, você deve restaurar o mesmo para seu estado original, podendo ser feito tanto no campo setup quanto no campo teardown. Neste exemplo você verá que o teardown irá remover todo HTML do elemento “container” após cada teste, fazendo com que todos testes sejam idênticos ao serem executados.

Testes assíncronos

O jsPerf tem suporte a testes assíncronos, mas vamos deixar bem claro aqui: não faça testes síncronos em casos que são assíncronos.

Vejamos um teste comparando função lenta com uma rápida dentro de um setTimeout:

setTimeout vs. setInterval · jsPerf

Se você não está habituado com testes unitários, deve estar se perguntando “Como assim?!? O segundo teste claramente demora muito mais, como pode ser avaliado como mais rápido?”. Isso é um comportamento bem estranho, por dois motivos, o teste está errado, deveria ser assíncrono e o outro, é que os dois resultados deveriam ser iguais, pois neste caso o que está sendo testado é o método setTimeout em ambos os casos, independente do seu callback (função passada no primeiro argumento do setTimeout).

Explicarei esta questão do enfileiramento de tarefas do Javascript em outra matéria, voltando aos testes assíncronos.

Então para testar uma função assíncrona, no local onde esta deveria executar seu callback final que indica o fim da operação em teste, você deve inserir o trecho de código:

 Javascript |  copiar código |? 
deferred.resolve();

Simples assim, como vemos neste teste do método setTimeout que diferente do primeiro está correto.

Corrigindo o primeiro teste, teríamos um teste, que resultaria em algo semelhante a isto:

setTimeout vs. setInterval · jsPerf

Exemplo prático seria com AJAX, utilizando a biblioteca jQuery:

 Javascript |  copiar código |? 
  1. $.ajax('url_query_testar', {
  2.   complete: function () {
  3.     deferred.resolve();
  4.   },
  5.   async: true
  6. });

Neste suposto teste que uso apenas para exemplificar, eu iria avaliar o tempo da requisição completa, não estaria testando desempenho do javascript, sim da latência do XHR, se eu colocar o defferd.resolve() no sucesso e esquecer de fazê-lo no erro, se um dos testes a requisição XHR falhar por algum motivo, o teste ficará aguardando o deferred.resolve() resultando em erro, pois nunca será executado.

Aleatoriedades não devem ser testadas

Quando você inserir um valor aleatório no campo setup ou no seu teste, como Math.random(), é provável que seu resultado será aleatório.

Mas as vezes é necessário usar valores aleatórios, em um exemplo anterior aqui mesmo há um teste que faz uso do Math.random().

Quando for necessário, defina os valores gerados aleatórios no campo HTML, salve-os em alguma variável para utilizar durante o teste. E no setup do seu teste, faça um clone do valor original (caso seja alterado durante o teste) e utilize a variável clonada no teste. Desta maneira você garante que os valores aleatórios serão gerados antes de começar os testes e todos os testes serão feitos com o mesmo grupo de valores preparados.

Fidelidade nos resultados

Vamos falar de alguns procedimentos para obter fidelidade nos resultados. Você pode simplesmente abrir uma nova aba e sair testando, mas isso não garante que seu resultado será fiel a realidade.

Neste teste por exemplo, eu prejudiquei os resultados do meu Chrome quando estava fazendo os testes. Abri uma nova aba e testei, o resultado demonstrou corretamente à proporção quanto a diferença de execução entre os casos, porém o desempenho ficou quase que pela metade do browser Arora, que utiliza JSC, sem Nitro (habilitado por padrão nas compilações em 64 bits do webkit no browser Safari e em sua versão para OS-X e iOS, gera grandes ganhos de desempenho utilizando JSC).

Ao fechar o browser, iniciá-lo sem abas e apenas executar o teste, houve diferença no resultado, agora este estava com resultados melhores do que o Arora, porém o gráfico continuou mostrando muito menos desempenho que o Firefox e Opera, o que é em parte uma falácia, pois os últimos resultados tiveram desempenho similares, pouquíssimo inferior ao do Opera. Isso acontece porque o jsPerf gera gráficos baseado nas médias dos resultados e não apenas no último resultado. Então como tenho 3 resultados ruins e depois 3 resultados bons, o gráfico está mostrando a média entre estes, que é algo entre o Arora e Opera.

Faça testes em ambiente controlado

Para obter melhores resultados no Chrome, utilize o flag para testes de desempenho e faça o teste ao abrir o browser, sem que outras janelas/abas tenham sido executadas. O seguinte flag irá expor a API chrome.Interval que permite uma avaliação mais precisa de tempo.

# linux
$ google-chrome --enable-benchmarking
# windows
chrome.exe --enable-benchmarking

Um teste não é o suficiente

Execute o teste mais de uma vez, para ter certeza que o resultado é válido, o uso de recursos do seu sistema operacional pode interferir no resultado do teste, então uma bateria de 3 testes irá lhe fornecer informações mais confiável.

Não mexa na barra de rolagem

Quando os testes estão sendo realizados, o jsPerf irá usar análises de tempo com a maior precisão possível, se você redimensiona o browser, ou mexer na barra de rolagem, isso causará disparo de eventos no DOM, que vão tornar seus testes menos precisos.

Devo usar uma VM pra fazer os testes?

Se quer testar browsers que não rodam nativos no seu sistema operacional, sim, você deve usar VM, isso vai facilitar sua vida, mas não precisa exagerar, criando uma VM para isolar totalmente seu teste, esse tipo de perfeição não trás grandes benefícios.

Dicas

Para testes ainda mais elaborados, você pode utilizar a API do Benchmark.js. Mas cuidado com as armadilhas, como a deste exemplo, onde o autor usou o this.count no setup e toda vez que alguém executar o teste, o contador vai incrementar e o resultado será cada vez pior.

Para executar os testes sem precisar clicar no botão “run”, adicione a URL #run e este vai executar os testes assim que a página estiver pronta para. Exemplo: http://jsperf.com/document-getelementbyid/2#run

Testes no Internet Explorer antes da versão 9, podem exibir caixas de diálogo com avisos quando estiver executando testes, isso acontece porque o IE até a versão 8 limitava os scripts a 5 milhões de instruções, o que hoje em dia é um valor trivial para se processar no Javascript. Por sorte a Microsoft fez uma correção que está disponível neste link.

É possível “exportar” os resultados clicando na imagem “Browserscope” e você ainda pode customizar a saída com a API do mesmo.document.getElementById() · jsPerfGostou? Quer contribuir ou ver se o jsPerf está sendo desenvolvido? Ele é openSource e seu código fonte está disponível no github.

Resumo

  • Utilize corretamente os campos HTML, setup e teardown;
  • Use o mínimo de código necessário dentro de cada campo de teste;
  • Tenha certeza que os métodos usados para teste estão comparando a mesma coisa (foco);
  • Se os métodos não estiverem comparando a mesma coisa, mencione isso na descrição;
  • Evite colar o código inteiro de sua biblioteca no campo HTML;
  • Jamais coloque o código da sua biblioteca no setup, se colocar no teste, tenha em mente que você vai testar o tempo de carregamento da biblioteca contra outras bibliotecas;
  • Valide seu código para garantir que você está testando o que você realmente quer testar;
  • Quando for reutilizar variáveis entre testes, redefina-as no setup ou teardown;
  • Limpe/prepare o DOM quando for utilizá-lo entre testes;
  • Não faça testes assíncronos como se fossem síncronos;
  • Não adicione aleatoriedades no seu teste;
  • Quando testes não forem conclusivos com o melhor algoritmo use o mais simples ou de melhor leitura;
  • Faça testes em ambiente controlado;
  • Faça mais de um teste para ter certeza.

Curiosidades

Cookies ou localStorage? No caso do jsPerf, localStorage, assim evita trafego desnecessário no protocolo HTTP.

Mas porque não salvar o rascunho de seus testes no localStorage? Afinal, se der problema posso perder todo meu trabalho.

Simples, isso afetaria testes que envolvem localStorage, então o mínimo de uso melhor, apenas os dados do usuário e só. E se você for testar localStorage, no setup, recomendo que apague os campos do jsPerf, para um teste fiel.

Referências