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:
- abra a rota;
- espere a interface estabilizar;
- abra e feche o menu;
- abra e feche um modal;
- avance e volte numa galeria;
- navegue para outra seção ou rota;
- volte ao estado inicial;
- 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 MBciclo 1: 24 MBciclo 2: 30 MBciclo 3: 36 MBEsses 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:
- carregue a página e espere estabilizar;
- tire o primeiro Heap snapshot;
- execute cinco ciclos da jornada;
- volte ao mesmo estado visual;
- tire o segundo snapshot;
- selecione a visualização Comparison;
- ordene por delta de tamanho e quantidade;
- 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;setIntervale 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;
ArrayBufferacumulado 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:
- gere uma build limpa;
- execute o ciclo cinco vezes;
- compare o gráfico de memória;
- tire snapshots antes e depois;
- confirme que o retainer desapareceu;
- teste desmontagem por clique, teclado e troca de rota;
- 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:
- consulte o contexto e a Version atuais;
- altere os arquivos dentro de
public/; - publique com
deploy; - acompanhe
deploy_statusaté um estado terminal; - abra a URL pública;
- repita a mesma jornada;
- 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.
