Performance no JavaScript
LidoEsta aula aborda a otimização de performance em JavaScript, com foco em medição precisa usando performance.now, compreensão de reflow e repaint no DOM, boas práticas de código e armadilhas comuns que afetam o desempenho. O objetivo é capacitar o aluno a identificar gargalos e escrever código mais eficiente.
Performance é um aspecto crucial no desenvolvimento web moderno. Usuários esperam interfaces rápidas e responsivas, e o JavaScript desempenha um papel central nisso. Nesta aula, vamos explorar como medir o desempenho do seu código, entender os mecanismos de renderização do navegador (reflow e repaint), aplicar boas práticas para escrever código eficiente e evitar armadilhas que podem degradar a experiência do usuário.
Dominar esses conceitos não só melhora a velocidade das suas aplicações, mas também contribui para uma melhor percepção de qualidade e acessibilidade. Vamos começar com a base: como medir o tempo de execução de forma confiável.
Medindo (performance.now)
Para otimizar, precisamos medir. O JavaScript oferece a API performance.now(), que retorna um timestamp de alta resolução em milissegundos, com precisão de submilissegundos. Diferente de Date.now(), que retorna o tempo desde 1970, performance.now() é relativo ao início da navegação da página e é monotônico, ou seja, não sofre ajustes de relógio do sistema.
Usar performance.now() é essencial para benchmarks precisos, pois evita erros de arredondamento e é mais confiável para medir trechos curtos de código. Veja um exemplo:
const inicio = performance.now();
// Código a ser medido
for (let i = 0; i < 1000000; i++) {
// operação qualquer
}
const fim = performance.now();
console.log(`Tempo de execução: ${fim - inicio} ms`);Além disso, a API performance.mark() e performance.measure() permitem criar marcos e medir intervalos entre eles, útil para análises mais complexas. Por exemplo:
performance.mark('inicio');
// ... código ...
performance.mark('fim');
performance.measure('minha-medida', 'inicio', 'fim');
const medidas = performance.getEntriesByName('minha-medida');
console.log(medidas[0].duration);Essas ferramentas são suportadas em todos os navegadores modernos e são a base para qualquer análise de performance.
Reflow/repaint
Reflow e repaint são processos de renderização do navegador. O reflow (ou layout) é o cálculo da posição e tamanho de cada elemento da página. O repaint é a redraw dos pixels na tela. Ambos são caros e devem ser minimizados para melhorar a performance.
Quando você modifica o DOM ou estilos, o navegador pode precisar recalcular o layout (reflow) e redesenhar (repaint). Por exemplo, alterar a propriedade width de um elemento causa reflow, enquanto alterar background-color causa apenas repaint. Reflow é mais custoso, pois afeta a geometria de outros elementos.
Para evitar reflows desnecessários, agrupe manipulações de DOM e estilos. Uma técnica comum é usar document.createDocumentFragment() para construir elementos fora do DOM e depois inseri-los de uma vez. Outra é usar display: none para remover o elemento do fluxo, fazer alterações e depois exibi-lo novamente.
Exemplo de código que causa múltiplos reflows:
const lista = document.getElementById('lista');
for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
item.textContent = 'Item ' + i;
lista.appendChild(item); // reflow a cada iteração
}Versão otimizada:
const lista = document.getElementById('lista');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
item.textContent = 'Item ' + i;
fragment.appendChild(item);
}
lista.appendChild(fragment); // apenas um reflowOutra armadilha é ler propriedades de layout (como offsetHeight) em um loop, forçando reflows síncronos. Prefira armazenar valores em variáveis antes de modificar o DOM.
Boas práticas
Existem várias boas práticas para escrever JavaScript de alto desempenho. Vamos listar as principais:
- Minimize manipulações do DOM: o DOM é lento. Use fragmentos,
innerHTMLcom moderação e evite acessar elementos repetidamente em loops. - Use variáveis locais: acessar variáveis locais é mais rápido que globais. Em loops, armazene propriedades frequentemente acessadas.
- Evite bloqueios no thread principal: operações síncronas pesadas (como grandes loops) travam a interface. Use
setTimeout,requestAnimationFrameou Web Workers para tarefas demoradas. - Use debounce e throttle: para eventos como scroll e resize, limite a frequência de execução das funções.
- Prefira estruturas de dados adequadas: por exemplo, use
SeteMapem vez de objetos para buscas frequentes. - Otimize loops: evite cálculos repetidos dentro do loop, use
forclássico em vez deforEachquando a performance for crítica. - Use a delegação de eventos: em vez de adicionar listeners a muitos elementos, adicione um listener ao pai e use
event.target.
Exemplo de otimização de loop:
// Ineficiente
const array = [1, 2, 3, 4, 5];
for (let i = 0; i < array.length; i++) {
console.log(array[i]);
}
// Otimizado
const array = [1, 2, 3, 4, 5];
for (let i = 0, len = array.length; i < len; i++) {
console.log(array[i]);
}Essas práticas, quando aplicadas consistentemente, reduzem o tempo de execução e melhoram a responsividade.
Armadilhas
Existem várias armadilhas que podem degradar a performance mesmo em código aparentemente simples. Vamos destacar as mais comuns:
- Forçar reflow com leituras síncronas: acessar
offsetWidth,offsetHeight,getBoundingClientRect(), etc., em um loop, força o navegador a recalcular o layout a cada leitura. Isso é extremamente caro. - Closures em loops: criar funções dentro de loops pode causar vazamentos de memória e lentidão. Use
letouforEachcom cuidado. - Excesso de escopo global: variáveis globais são acessadas mais lentamente e podem causar colisões. Use módulos ou IIFEs.
- Uso excessivo de
console.log: em produção, logs desnecessários podem impactar a performance, especialmente em loops. - Manipulação de strings em loops: usar
+=para concatenar muitas strings é ineficiente. UseArray.join()ou template literals. - Ignorar o garbage collector: criar muitos objetos temporários pode aumentar a frequência de coletas, causando pausas. Tente reutilizar objetos.
Exemplo de armadilha com leitura de layout:
// Ruim: lê offsetHeight a cada iteração
const items = document.querySelectorAll('.item');
for (let i = 0; i < items.length; i++) {
console.log(items[i].offsetHeight); // força reflow
}
// Bom: lê uma vez e armazena
const items = document.querySelectorAll('.item');
const heights = [];
for (let i = 0; i < items.length; i++) {
heights.push(items[i].offsetHeight); // ainda força reflow, mas apenas uma vez
}Na verdade, a leitura em si força reflow, mas se você fizer várias leituras seguidas sem alterar o DOM, o navegador pode otimizar. O problema é intercalar leituras com escritas. Evite isso sempre que possível.
Boas práticas adicionais e observações finais
Além das práticas já mencionadas, é importante monitorar a performance continuamente. Use as ferramentas do desenvolvedor (DevTools) para gravar perfis e identificar gargalos. O Lighthouse também oferece auditorias de performance com sugestões acionáveis.
Lembre-se de que a otimização prematura pode ser contraproducente. Primeiro, escreva código claro e correto; depois, meça e otimize apenas onde for necessário. A regra 80/20 se aplica: a maioria dos ganhos vem de poucos pontos críticos.
Por fim, mantenha-se atualizado com as mudanças nos navegadores e nas APIs. O que era lento ontem pode ser rápido hoje, e vice-versa. Teste sempre em múltiplos ambientes.
Referências
- MDN: performance.now()
- MDN: performance.mark()
- MDN: performance.measure()
- MDN: Caminho crítico de renderização
- Google Developers: Rendimento de renderização
- web.dev: Aprenda performance
- JavaScript.info
Exercícios
- Escreva uma função que mede o tempo de execução de um trecho de código usando
performance.now()e exibe o resultado no console. Teste com um loop de 100.000 iterações. - Explique a diferença entre reflow e repaint e dê um exemplo de cada.
- Refatore o seguinte código para evitar múltiplos reflows:
const ul = document.querySelector('ul'); for (let i = 0; i < 100; i++) { const li = document.createElement('li'); li.textContent = 'Item ' + i; ul.appendChild(li); } - Liste três boas práticas para melhorar a performance de JavaScript em páginas web.
- Qual é a armadilha de usar
Date.now()em vez deperformance.now()para medir tempo de execução?
function medir(fn) {
const inicio = performance.now();
fn();
const fim = performance.now();
console.log(`Tempo: ${fim - inicio} ms`);
}
medir(() => {
let soma = 0;
for (let i = 0; i < 100000; i++) {
soma += i;
}
console.log('Soma:', soma);
});const ul = document.querySelector('ul');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const li = document.createElement('li');
li.textContent = 'Item ' + i;
fragment.appendChild(li);
}
ul.appendChild(fragment);Date.now() tem resolução em milissegundos (não submilissegundos) e pode sofrer ajustes de relógio, enquanto performance.now() é monotônico e de alta resolução, adequado para medições precisas.