Nesta aula, vamos mergulhar no funcionamento da memória em JavaScript, focando no garbage collector (GC) e nos problemas que podem surgir quando a memória não é gerida corretamente. Entender como o GC opera é essencial para escrever código eficiente e evitar vazamentos que degradam a performance da aplicação.

O JavaScript é uma linguagem com gerenciamento de memória automático: o desenvolvedor não precisa alocar ou liberar memória manualmente. No entanto, isso não significa que podemos ignorar a memória; decisões de design podem levar a retenções indesejadas que sobrecarregam o GC e causam lentidão. Vamos aprender a reconhecer esses cenários e a investigar problemas de memória com ferramentas práticas.

Como o GC funciona

O garbage collector (GC) é o componente responsável por liberar memória que não é mais utilizada pelo programa. Em JavaScript, a memória é alocada para objetos, strings, closures e outras estruturas. O GC identifica quais objetos estão inacessíveis a partir das raízes (como variáveis globais, pilha de execução e referências de escopo) e os remove, liberando espaço.

O mecanismo mais comum é a marcação e varredura (mark-and-sweep). O GC percorre o grafo de objetos a partir das raízes, marcando todos os objetos alcançáveis. Depois, varre a memória e desaloca os objetos não marcados. Esse processo ocorre em ciclos, e o timing é controlado pelo runtime (V8 no Chrome/Node.js, SpiderMonkey no Firefox, etc.).

Um conceito importante é a geração: objetos recém-criados são colocados em uma geração jovem, que é coletada com mais frequência. Objetos que sobrevivem a várias coletas são promovidos para a geração antiga, coletada com menos frequência. Isso otimiza o desempenho, pois a maioria dos objetos tem vida curta.

Vamos ver um exemplo simples de como referências mantêm objetos vivos:

let objeto = { nome: 'exemplo' };
let copia = objeto; // 'copia' referencia o mesmo objeto
objeto = null; // 'objeto' não referencia mais, mas 'copia' ainda mantém vivo
console.log(copia.nome); // 'exemplo' - o objeto não foi coletado

Aqui, mesmo após definir objeto = null, o objeto continua acessível por meio da variável copia, então não é coletado. O GC só libera a memória quando não há mais referências alcançáveis.

Vazamentos comuns

Um vazamento de memória ocorre quando objetos não são mais necessários, mas ainda são referenciados, impedindo que o GC os libere. Isso pode levar ao aumento contínuo do uso de memória e, eventualmente, a travamentos. Vamos listar os vazamentos mais comuns em JavaScript.

Variáveis globais acidentais: Atribuir um valor a uma variável sem declará-la (sem let, const ou var) cria uma propriedade no objeto global (window no navegador). Isso mantém a referência enquanto a página estiver aberta.

function criarVazamento() {
    vazamento = { dados: new Array(1000000) }; // sem declaração
}
criarVazamento();
// 'vazamento' agora é uma propriedade de window, nunca será coletado

Closures não intencionais: Closures capturam o escopo externo, mantendo referências a variáveis que podem ser grandes. Se uma closure é armazenada em um lugar de longa duração, ela mantém o escopo vivo.

function criarClosure() {
    const dadosGrandes = new Array(1000000);
    return function() {
        console.log(dadosGrandes.length);
    };
}
const funcao = criarClosure();
// 'dadosGrandes' fica retido enquanto 'funcao' existir

Timers e listeners não limpos: setInterval, setTimeout e event listeners que não são removidos mantêm referências a objetos. Em SPAs, é comum adicionar listeners sem removê-los ao destruir componentes.

function iniciarTimer() {
    const dados = { /* ... */ };
    setInterval(() => {
        // usa 'dados'
    }, 1000);
}
iniciarTimer();
// O timer nunca é limpo, mantendo 'dados' vivo para sempre

Referências a DOM desconectado: Quando um elemento é removido do DOM, mas ainda há referências JavaScript a ele, ele permanece na memória. Isso é comum em bibliotecas de manipulação de DOM.

const elemento = document.getElementById('meu-elemento');
document.body.removeChild(elemento);
// 'elemento' ainda referencia o nó removido, impedindo a coleta

Caches sem limite: Mapas ou arrays usados como cache podem crescer indefinidamente se não houver política de remoção.

const cache = new Map();
function processar(id) {
    if (!cache.has(id)) {
        cache.set(id, { dados: /* ... */ });
    }
    return cache.get(id);
}
// Se 'processar' é chamado com muitos ids diferentes, o cache cresce sem limite

Como investigar

Para identificar vazamentos de memória, precisamos usar ferramentas de profiling. O Chrome DevTools oferece o Memory panel, que permite tirar snapshots de heap, gravar atividades e analisar retenção de objetos.

Gravando e comparando snapshots: Tire um snapshot inicial, execute ações que suspeita causar vazamento, tire outro snapshot e compare. Objetos que aparecem no segundo snapshot e não no primeiro podem ser vazamentos.

// Exemplo de código que pode vazar
const itens = [];
function adicionarItem() {
    itens.push({ dados: new Array(10000) });
}
// Chame adicionarItem() várias vezes e observe o heap

Usando a gravação de alocações: No DevTools, a opção "Record allocation timeline" mostra onde as alocações ocorrem ao longo do tempo. Isso ajuda a identificar picos de memória e funções que alocam demais.

Analisando a árvore de retenção: Ao inspecionar um objeto em um snapshot, você pode ver a cadeia de referências que o mantém vivo. Isso revela a causa raiz do vazamento.

Ferramentas no Node.js: O Node.js oferece o módulo v8 e a flag --inspect para usar o DevTools. Além disso, o process.memoryUsage() fornece estatísticas de memória.

console.log(process.memoryUsage());
// { rss, heapTotal, heapUsed, external, arrayBuffers }

Uso de WeakMap e WeakSet: Essas estruturas permitem que o GC colete chaves se não houver outras referências. São úteis para caches sem vazar.

const cache = new WeakMap();
function processar(objeto) {
    if (!cache.has(objeto)) {
        cache.set(objeto, { resultado: /* ... */ });
    }
    return cache.get(objeto);
}
// Quando 'objeto' não for mais referenciado, a entrada no WeakMap pode ser coletada

Boas práticas

  • Sempre declare variáveis com let, const ou var para evitar globais acidentais.
  • Limpe timers e listeners quando não forem mais necessários, especialmente em componentes de UI.
  • Evite caches sem limite; use WeakMap ou implemente políticas de remoção (LRU).
  • Remova referências a elementos DOM após removê-los da árvore.
  • Use closures com cuidado: evite capturar variáveis grandes desnecessariamente.

Exercícios

  1. Explique por que o código a seguir pode causar vazamento de memória e como corrigi-lo:
    function iniciar() {
        const dados = { nome: 'teste' };
        setInterval(() => {
            console.log(dados.nome);
        }, 1000);
    }
    iniciar();
    

    ✓ Resposta: O setInterval mantém a closure que captura dados vivo para sempre, pois o timer nunca é limpo. Para corrigir, devemos limpar o timer quando não for mais necessário:
    let timer;
    function iniciar() {
        const dados = { nome: 'teste' };
        timer = setInterval(() => {
            console.log(dados.nome);
        }, 1000);
    }
    // Em algum momento, chame clearInterval(timer);
    
  2. Considere o código a seguir. Qual vazamento potencial existe? Como usar WeakMap para evitar?
    const cache = new Map();
    function buscar(objeto) {
        if (!cache.has(objeto)) {
            cache.set(objeto, { resultado: 'valor' });
        }
        return cache.get(objeto);
    }
    

    ✓ Resposta: O Map mantém referências fortes às chaves, então os objetos nunca serão coletados, mesmo se não forem mais usados. Trocar por WeakMap permite que o GC colete as chaves quando não houver outras referências:
    const cache = new WeakMap();
    function buscar(objeto) {
        if (!cache.has(objeto)) {
            cache.set(objeto, { resultado: 'valor' });
        }
        return cache.get(objeto);
    }
    
  3. Escreva um trecho de código que remova corretamente um event listener para evitar vazamento.

    ✓ Resposta: Para remover um listener, é necessário manter a referência à função:
    const botao = document.getElementById('meu-botao');
    function aoClicar() {
        console.log('Clicado');
    }
    botao.addEventListener('click', aoClicar);
    // Em algum momento, remover:
    botao.removeEventListener('click', aoClicar);
    
  4. O que a ferramenta "Heap Snapshot" no Chrome DevTools faz e como ela ajuda a investigar vazamentos?

    ✓ Resposta: O Heap Snapshot captura uma imagem da memória heap em um determinado momento, listando todos os objetos e suas referências. Ao comparar snapshots antes e depois de uma ação, podemos identificar objetos que foram alocados e não liberados, e analisar a árvore de retenção para descobrir o que os mantém vivos.
  5. Dê um exemplo de vazamento por variável global acidental e como evitá-lo.

    ✓ Resposta: Exemplo:
    function criar() {
        dados = { grande: new Array(1000000) }; // sem declaração
    }
    
    A variável dados vira uma propriedade de window e nunca é coletada. Para evitar, declare com let ou const:
    function criar() {
        const dados = { grande: new Array(1000000) };
    }
    

Referências