W3docs

Otimização de Performance no Desenvolvimento Web

Aprenda técnicas essenciais de otimização de performance para DOM em JavaScript: minimize acessos, evite layout thrashing e use DocumentFragment.

O DOM é o elemento mais custoso que a maioria das aplicações JavaScript manipula. Ler uma propriedade como offsetHeight pode forçar o navegador a parar e recalcular a geometria da página, e escrever no DOM pode disparar um reflow (recálculo de posições e tamanhos dos elementos) e um repaint (redesenho dos pixels). Fazer isso descuidadamente dentro de um loop transforma uma página que deveria ser instantânea em algo que trava.

Este guia explica por que as operações no DOM são lentas e apresenta técnicas concretas para corrigi-las: minimizar o acesso, evitar layout thrashing, agrupar alterações com requestAnimationFrame e document.createDocumentFragment(), e perfilar o que você não consegue adivinhar.

Por Que as Operações no DOM São Lentas

O JavaScript roda em um engine rápido, mas o DOM é a fronteira entre esse engine e o pipeline de renderização do navegador. Cruzar essa fronteira repetidamente é o que causa problemas:

  • Reflow (layout) — o navegador recalcula a posição e o tamanho dos elementos. Um reflow em um elemento pode se propagar para seus ancestrais, descendentes e irmãos.
  • Repaint — o navegador redesenha os pixels (cores, sombras, visibilidade) sem alterar a geometria. Mais barato que o reflow, mas ainda tem custo.

O ponto central: o navegador tenta agrupar essas operações por você. Ele enfileira suas escritas e as executa de uma vez, logo antes do próximo frame ser pintado. Você quebra essa otimização no momento em que uma propriedade de layout, porque o navegador precisa executar todas as escritas pendentes imediatamente para fornecer uma resposta precisa. Esse flush forçado é chamado de layout síncrono (forçado), e fazê-lo dentro de um loop é a raiz da maioria dos problemas de performance com DOM.

Minimizando o Acesso ao DOM

Cada leitura e escrita de propriedade cruza a fronteira entre JS e DOM, portanto a otimização mais barata é fazer menos disso.

  • Guarde referências de elementos em cache. Localize um elemento uma única vez e armazene-o em uma variável, em vez de chamar document.querySelector a cada iteração.
  • Leia em variáveis locais. Contadores de loop, tamanhos e valores calculados pertencem a variáveis JavaScript, e não devem ser relidos do DOM a cada passagem.
  • Construa strings, não nós, quando apropriado. Atribuir innerHTML uma única vez costuma ser mais rápido do que inserir muitos nós individualmente — embora isso perca os event listeners e seja inseguro com entradas não confiáveis.
// Slow: re-queries and re-reads the DOM on every iteration
for (let i = 0; i < items.length; i++) {
  document.getElementById('list').appendChild(makeRow(items[i]));
}

// Fast: resolve the reference once, outside the loop
const list = document.getElementById('list');
for (let i = 0; i < items.length; i++) {
  list.appendChild(makeRow(items[i]));
}

Veja Selecting DOM Elements para escolher seletores rápidos e específicos — prefira getElementById e querySelector direcionado a cadeias de descendentes profundas como div > ul li span.

Tratamento Eficiente de Eventos

Associar um listener a cada item de uma lista longa desperdiça memória e torna as atualizações do DOM mais lentas. A delegação de eventos associa um único listener a um pai compartilhado e usa event.target para identificar qual filho foi clicado, aproveitando o event bubbling.

// One listener handles the whole list, including rows added later
document.getElementById('list').addEventListener('click', (event) => {
  const row = event.target.closest('li');
  if (row) console.log('clicked row:', row.dataset.id);
});

Isso escala para milhares de elementos e automaticamente cobre itens adicionados após a configuração do listener. Saiba mais em Event Handling in the DOM e Introduction to Browser Events.

Entendendo e Evitando o Layout Thrashing

O Que É Layout Thrashing?

O layout thrashing acontece quando você intercala leituras e escritas de propriedades de layout em rápida sucessão. Cada leitura força um layout síncrono para executar a escrita anterior, fazendo com que um loop de leitura-escrita-leitura-escrita dispare um reflow por iteração em vez de um reflow total.

// Bad: read (offsetWidth) forces layout, then write invalidates it — every loop
const boxes = document.querySelectorAll('.box');
boxes.forEach((box) => {
  box.style.width = box.offsetWidth + 10 + 'px'; // read + write interleaved
});

Como Corrigir: Agrupe Leituras, Depois Escritas

Agrupe todas as leituras primeiro, depois realize todas as escritas. O navegador faz uma passagem de layout para as leituras e outra para as escritas.

const boxes = document.querySelectorAll('.box');

// 1. Read phase — collect every measurement first
const widths = [...boxes].map((box) => box.offsetWidth);

// 2. Write phase — now apply all changes; no read interrupts them
boxes.forEach((box, i) => {
  box.style.width = widths[i] + 10 + 'px';
});

Agendando Trabalho com requestAnimationFrame

requestAnimationFrame executa seu callback logo antes do próximo repaint, que é o momento ideal para escrever mudanças no DOM — elas são mescladas em um único frame em vez de disparar pinturas intermediárias.

const element = document.getElementById('box');

requestAnimationFrame(() => {
  const width = element.offsetWidth; // read
  element.style.width = width + 10 + 'px'; // write, applied in the same frame
});

Para animações, mantenha todas as escritas no DOM dentro do callback de requestAnimationFrame e nunca leia o layout no meio delas.

Agrupando Alterações no DOM

Usando document.createDocumentFragment()

document.createDocumentFragment() é um contêiner leve e fora da tela. Nós adicionados a um fragment não fazem parte do documento ativo, portanto construí-lo não dispara reflows. Quando você adiciona o fragment finalizado à página, o navegador insere todos os seus filhos em uma única operação — um reflow em vez de um por nó.

Exemplo

<!DOCTYPE html>
<html>
<head>
    <title>Batching DOM Changes</title>
</head>
<body>
    <div id="container"></div>

    <script>
        const container = document.getElementById('container');
        const fragment = document.createDocumentFragment();

        for (let i = 0; i < 40; i++) {
            const div = document.createElement('div');
            div.textContent = `Item ${i}`;
            fragment.appendChild(div);
        }

        container.appendChild(fragment); // Batch update
    </script>
</body>
</html>

O loop constrói 40 elementos no fragment sem nenhum impacto na página visível. Apenas o container.appendChild(fragment) final toca o DOM ativo, então o navegador executa uma única passagem de layout em vez de 40. (Engines modernos também permitem adicionar um array de nós em uma única chamada com container.append(...nodes), que é agrupada de forma semelhante.)

Evitando o Layout Síncrono Forçado

Uma versão sutil do thrashing é ler uma propriedade de layout logo após uma mudança de estilo, o que força o navegador a recalcular o layout imediatamente:

const box = document.getElementById('box');

box.classList.add('expanded'); // write — queues a layout change
const height = box.offsetHeight; // read — forces layout NOW to answer

Se você não precisa do valor imediatamente, adie a leitura para o próximo frame com requestAnimationFrame, ou reestruture o código para que todas as leituras ocorram antes de qualquer escrita. Propriedades comuns que disparam layout forçado quando lidas incluem offsetTop/offsetWidth/offsetHeight, clientWidth/clientHeight, scrollTop e getComputedStyle().

Boas Práticas

  1. Adie JavaScript não crítico. Adicione o atributo defer às tags <script> para que o navegador continue analisando o HTML e execute o script após o DOM estar pronto. Use async para scripts de terceiros independentes.
  2. Prefira alternância de classes em vez de estilos inline. Alterar uma classe CSS permite que o navegador aplique muitas regras de estilo em um único reflow, em vez de um reflow por atribuição de style inline.
  3. Anime transform e opacity. Elas são compostas pela GPU e ignoram completamente o layout e o paint, diferentemente de animar width, top ou margin.
  4. Desanexe, modifique, reanexe. Para edições pesadas, remova uma subárvore do documento (ou oculte-a com display: none), altere-a fora da tela e depois reinsira-a.
  5. Perfile antes de otimizar. Use o painel Performance do DevTools do navegador para encontrar o verdadeiro gargalo em vez de adivinhar — veja DOM Debugging and Tools.
Informação

A regra de ouro: agrupe suas leituras do DOM juntas, depois agrupe suas escritas juntas. Toda vez que uma leitura ocorre após uma escrita, o navegador é forçado a recalcular o layout de forma síncrona. Agrupá-las permite que ele faça o trabalho uma vez por frame.

Armadilhas Comuns

  • Chamar document.querySelector dentro de um loop em vez de guardar o resultado em cache.
  • Ler offsetWidth/offsetHeight e escrever estilos na mesma iteração do loop.
  • Associar um event listener separado a cada item de uma lista grande e dinâmica em vez de usar delegação.
  • Animar propriedades que disparam layout (width, left, margin) em vez de transform/opacity.

Conclusão

A performance do DOM se resume a uma ideia: o navegador é rápido em agrupar seu trabalho de renderização, e sua tarefa é evitar quebrar esse agrupamento. Guarde referências em cache, delegue eventos, agrupe leituras antes das escritas, construa grandes atualizações dentro de um DocumentFragment e agende mudanças visuais com requestAnimationFrame. Em caso de dúvida, perfile — o painel Performance do DevTools mostrará exatamente onde os reflows estão ocorrendo. Em seguida, aprofunde seu conjunto de ferramentas com DOM Manipulation e Advanced DOM Techniques.

Prática

Prática
Quais das técnicas a seguir são importantes para otimizar a performance do DOM?
Quais das técnicas a seguir são importantes para otimizar a performance do DOM?
Was this page helpful?