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 reflow

Outra 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, innerHTML com 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, requestAnimationFrame ou 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 Set e Map em vez de objetos para buscas frequentes.
  • Otimize loops: evite cálculos repetidos dentro do loop, use for clássico em vez de forEach quando 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 let ou forEach com 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. Use Array.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

Exercícios

  1. 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.
  2. ✓ Resposta:
    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);
    });
  3. Explique a diferença entre reflow e repaint e dê um exemplo de cada.
  4. ✓ Resposta: Reflow é o cálculo do layout (posição e tamanho) dos elementos, enquanto repaint é a redraw dos pixels. Exemplo de reflow: alterar a largura de um elemento. Exemplo de repaint: alterar a cor de fundo.
  5. 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);
    }
  6. ✓ Resposta:
    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);
  7. Liste três boas práticas para melhorar a performance de JavaScript em páginas web.
  8. ✓ Resposta: 1. Minimizar manipulações do DOM usando fragmentos. 2. Usar debounce/throttle para eventos frequentes. 3. Evitar leituras de layout síncronas em loops.
  9. Qual é a armadilha de usar Date.now() em vez de performance.now() para medir tempo de execução?
  10. ✓ Resposta: 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.