O site abre rápido. Depois de navegar, abrir modais e trocar filtros por alguns minutos, a rolagem começa a travar. O ventilador acelera. Em um celular com pouca memória, a aba recarrega ou fecha.

Esse padrão pode indicar um vazamento de memória, mas também pode ser uma página naturalmente pesada ou uma coleta de lixo frequente. A diferença está no que continua alcançável depois que a jornada terminou.

Não tente resolver olhando apenas o tamanho do bundle. Repita uma ação, deixe o navegador coletar o que não é mais usado e descubra qual referência manteve o restante vivo.

Separe três problemas

Memória alta não significa sempre vazamento.

Problema Padrão observado
vazamento uso cresce após cada ciclo e não volta
excesso de memória uso fica alto, mas estabiliza
coleta de lixo frequente gráfico sobe e cai com pausas perceptíveis

Uma galeria com imagens grandes pode manter um patamar alto sem crescer. Um modal que deixa nós e listeners para trás aumenta a linha de base a cada abertura.

A documentação do Chrome DevTools sobre problemas de memória recomenda observar a evolução, os nós DOM destacados e as referências que impedem a coleta.

Escolha uma jornada repetível

Vazamento aparece por repetição. Defina um ciclo que represente o bug:

  1. abra a rota;
  2. espere a interface estabilizar;
  3. abra e feche o menu;
  4. abra e feche um modal;
  5. avance e volte numa galeria;
  6. navegue para outra seção ou rota;
  7. volte ao estado inicial;
  8. repita o ciclo cinco vezes.

Não misture dez funções na primeira captura. Um ciclo curto ajuda a relacionar o crescimento ao componente responsável.

Registre:

URL e Version:
navegador e versão:
ação repetida:
quantidade de ciclos:
heap inicial:
heap depois de cada ciclo:
nós DOM depois de cada ciclo:
sintoma percebido:

Observe a tendência no Performance

Abra o painel Performance, ative Memory e grave os ciclos. O gráfico mostra JS heap, documentos, nós DOM, listeners e outros contadores conforme a captura.

Alocações durante uma ação são normais. Procure o piso depois da coleta:

ciclo 0: 18 MB
ciclo 1: 24 MB
ciclo 2: 30 MB
ciclo 3: 36 MB

Esses números são um exemplo de padrão, não um limite. O valor aceitável depende do conteúdo, do navegador e do aparelho. O sinal importante é o crescimento repetido ligado à mesma jornada.

Se o heap volta perto do início, mas a página continua lenta, investigue CPU, layout e pintura. O guia de JavaScript na thread principal ajuda a separar memória de tarefas longas.

Compare heap snapshots

O painel Memory permite capturar o heap em momentos específicos.

Use este roteiro:

  1. carregue a página e espere estabilizar;
  2. tire o primeiro Heap snapshot;
  3. execute cinco ciclos da jornada;
  4. volte ao mesmo estado visual;
  5. tire o segundo snapshot;
  6. selecione a visualização Comparison;
  7. ordene por delta de tamanho e quantidade;
  8. abra os objetos que continuaram vivos.

Capturar um snapshot inicia uma coleta de lixo. Isso reduz o ruído de objetos que já poderiam ser liberados.

A referência de heap snapshots explica as visões Summary, Comparison e Containment. A comparação confirma o delta. A árvore de retenção explica por que o objeto não saiu.

Procure DOM destacado

Um nó destacado saiu da árvore visível, mas alguma referência JavaScript ainda aponta para ele.

O caso aparece com frequência quando um componente:

  • remove um modal sem remover listeners globais;
  • guarda elementos em um array ou Map;
  • deixa uma closure capturar a subárvore;
  • mantém um observer conectado;
  • registra callbacks num SDK;
  • substitui uma seção sem encerrar a instância anterior.

No painel Memory, use Detached elements ou filtre o snapshot por Detached. Selecione um nó e siga a cadeia de retenção até a referência mais próxima do código do projeto.

Não conclua que todo nó destacado é um bug. O DevTools também mantém objetos selecionados no console. Repita a captura em uma janela limpa e sem variáveis temporárias antes de editar.

Siga a referência retentora

O objeto grande nem sempre é a causa. Uma pequena função pode manter uma árvore inteira viva.

Exemplo:

const retainedPanels = new Map();
function mountPanel(id, element) {
retainedPanels.set(id, element);
}
function unmountPanel(id) {
document.querySelector(`[data-panel="${id}"]`)?.remove();
}

O nó sai do DOM, mas continua no Map. A desmontagem precisa remover a referência.

function unmountPanel(id) {
retainedPanels.get(id)?.remove();
retainedPanels.delete(id);
}

Esse exemplo é simples. Em frameworks, a referência pode ficar num store, cache, portal, subscription ou closure. Siga o retainer no snapshot em vez de apagar estruturas ao acaso.

Remova listeners com o mesmo contrato

Um listener global pode reter o componente que o criou.

function mountMenu(menu) {
function onKeydown(event) {
if (event.key === "Escape") menu.close();
}
window.addEventListener("keydown", onKeydown);
return () => window.removeEventListener("keydown", onKeydown);
}

A função de limpeza precisa receber a mesma referência usada no registro. Criar outra função inline no removeEventListener não remove a original.

Outra opção é agrupar listeners com AbortController quando o suporte do projeto permitir:

function mountMenu(menu) {
const controller = new AbortController();
window.addEventListener(
"keydown",
(event) => {
if (event.key === "Escape") menu.close();
},
{ signal: controller.signal },
);
return () => controller.abort();
}

Escolha um mecanismo e faça a desmontagem acontecer em todos os caminhos, inclusive erro, troca de rota e fechamento por teclado.

Desconecte observers, timers e subscriptions

Revise recursos que continuam produzindo trabalho:

  • IntersectionObserver;
  • ResizeObserver;
  • MutationObserver;
  • setInterval e timers recorrentes;
  • subscriptions de stores;
  • callbacks de SDKs;
  • streams e conexões;
  • workers;
  • URLs criadas com URL.createObjectURL.

Cada API tem uma forma de encerrar. Use disconnect, clearInterval, unsubscribe, terminate, close ou URL.revokeObjectURL conforme o contrato real.

Não adicione uma função de limpeza que nunca é chamada. O teste precisa provar que o contador e o heap estabilizam depois do ciclo.

Grave alocações quando o snapshot não basta

Heap snapshots mostram o que ficou vivo. Allocation instrumentation on timeline mostra quando os objetos foram alocados e quais sobreviveram até o fim da gravação.

Use quando:

  • a classe retida tem nome genérico;
  • a jornada cria muitos objetos parecidos;
  • a alocação ocorre longe do componente visível;
  • o crescimento depende de tempo;
  • o snapshot fica grande demais para navegar.

Grave apenas a ação suspeita. Depois selecione um intervalo e leia a pilha de alocação. Allocation sampling impõe menos custo e ajuda em sessões longas, mas oferece uma aproximação.

Teste imagens, canvas e buffers

Nem toda pressão aparece como objetos JavaScript grandes. Canvas, imagens decodificadas, ArrayBuffer, vídeo e recursos de GPU podem elevar o consumo do processo.

Procure:

  • canvas que cresce a cada renderização;
  • imagens carregadas e mantidas em caches sem limite;
  • ArrayBuffer acumulado por downloads;
  • previews com object URLs nunca revogadas;
  • players recriados sem destruir a instância anterior;
  • WebGL com recursos não liberados.

Compare a coluna Memory footprint do Task Manager com o JS memory. Se o processo cresce enquanto o heap JavaScript estabiliza, investigue recursos fora do heap.

O artigo sobre vídeos e iframes mostra como adiar players pesados. Para uma página com DOM crescente, use o guia de DOM excessivo.

Valide em um aparelho com pouca memória

O computador pode esconder um vazamento pequeno. Depois da correção local, repita o ciclo num celular real que represente o público.

Observe:

  • aumento de pausas depois de cada ciclo;
  • perda de frames na rolagem;
  • aquecimento;
  • aba descartada ou recarregada;
  • processo encerrado pelo sistema;
  • diferença entre primeira e quinta repetição.

O guia de teste em celulares reais explica como registrar aparelho, navegador, estado térmico e Version.

Confirme a correção pelo mesmo ciclo

Depois de editar:

  1. gere uma build limpa;
  2. execute o ciclo cinco vezes;
  3. compare o gráfico de memória;
  4. tire snapshots antes e depois;
  5. confirme que o retainer desapareceu;
  6. teste desmontagem por clique, teclado e troca de rota;
  7. repita no aparelho mais limitado da matriz.

O objetivo não é fazer o heap terminar exatamente no valor inicial. Navegadores mantêm caches e estruturas internas. A correção passa quando o crescimento deixa de acompanhar cada repetição e o comportamento continua correto.

Publique como uma mudança isolada

Não misture a correção com uma troca de framework ou redesenho completo. Uma alteração pequena preserva a relação entre causa, diff e resultado.

No Site no Ar:

  1. consulte o contexto e a Version atuais;
  2. altere os arquivos dentro de public/;
  3. publique com deploy;
  4. acompanhe deploy_status até um estado terminal;
  5. abra a URL pública;
  6. repita a mesma jornada;
  7. compare a nova Version com a linha de base.

A documentação de Publicação descreve o contrato de arquivos e Versions. O guia de orçamento de performance ajuda a transformar o ciclo em um gate de regressão adequado ao projeto.

Prompt para investigar memória

Investigue um possível vazamento de memória neste site sem publicar ainda.
URL e Version: [URL e identificador]
Sintoma: [lentidão, aba recarregada ou crescimento]
Jornada repetível: [ações]
1. Grave cinco ciclos no Performance com Memory ativado.
2. Separe vazamento, excesso de memória e coleta frequente.
3. Compare heap snapshots antes e depois da mesma jornada.
4. Procure DOM destacado e siga a cadeia de retenção.
5. Revise listeners, observers, timers, subscriptions e object URLs.
6. Corrija uma referência e repita o ciclo nas mesmas condições.
7. Teste num aparelho com pouca memória quando disponível.
8. Só proponha a publicação com heap, retainer e comportamento comparados.

Um vazamento não se prova com uma captura impressionante. Ele se prova quando a mesma ação deixa objetos vivos, a cadeia de retenção mostra por quê e a correção faz o crescimento parar sem quebrar a interface.