Em JavaScript, o gerenciamento de memória é feito automaticamente pelo garbage collector (GC). Normalmente, não precisamos nos preocupar com isso, mas existem situações em que seria útil manter uma referência 'fraca' a um objeto, que não impeça sua coleta, ou ser notificado quando um objeto for coletado. É exatamente isso que as APIs WeakRef e FinalizationRegistry oferecem.

Essas APIs são avançadas e destinadas a casos de uso específicos, como implementação de caches, observadores de memória ou integração com recursos externos. Nesta aula, vamos entender seu funcionamento, quando usá-las com segurança e quando evitá-las, com exemplos práticos e boas práticas.

O que são

WeakRef (Weak Reference) é um objeto que mantém uma referência 'fraca' a outro objeto, chamado de alvo (target). Isso significa que a referência não impede que o alvo seja coletado pelo garbage collector. Se não houver outras referências fortes ao objeto, ele pode ser coletado a qualquer momento, e a WeakRef passa a retornar undefined quando você tenta acessá-lo.

FinalizationRegistry é uma ferramenta que permite registrar um callback (chamado de 'finalization callback') para ser executado quando um objeto registrado é coletado pelo GC. Isso é útil para limpar recursos associados, como fechar conexões, liberar memória externa ou notificar um sistema externo.

Ambas as APIs são construtores que devem ser usados com a palavra-chave new. A WeakRef recebe o objeto alvo, e o FinalizationRegistry recebe a função de callback. Exemplo básico:

const alvo = { nome: 'exemplo' };
const ref = new WeakRef(alvo);

const registry = new FinalizationRegistry((heldValue) => {
  console.log('Objeto coletado:', heldValue);
});
registry.register(alvo, 'valor associado');

Neste exemplo, criamos uma WeakRef para o objeto alvo e registramos um callback no FinalizationRegistry. Quando o objeto for coletado, o callback será chamado com o valor associado (heldValue).

Quando (não) usar

O uso de WeakRef e FinalizationRegistry é extremamente delicado, pois dependem do comportamento não-determinístico do garbage collector. Isso significa que o momento exato da coleta não é garantido, e pode variar entre motores JavaScript (V8, SpiderMonkey, JavaScriptCore) e até entre execuções. Portanto, essas APIs não devem ser usadas para lógica crítica de negócios.

Quando usar:

  • Caches: Para implementar caches que não retêm objetos desnecessariamente. Por exemplo, um cache de objetos grandes que podem ser recriados se necessário.
  • Observação de memória: Para diagnosticar vazamentos de memória ou monitorar o ciclo de vida de objetos.
  • Integração com recursos externos: Para limpar recursos nativos (como arquivos, conexões) quando o objeto JavaScript for coletado.

Quando NÃO usar:

  • Lógica de negócios: Nunca dependa de que um objeto será coletado em um momento específico. O GC não é previsível.
  • Como substituto de referências fortes: Se você precisa acessar o objeto com frequência, use referências normais.
  • Para garantir limpeza de recursos: O FinalizationRegistry não garante que o callback será chamado antes do programa terminar. Para recursos críticos, use padrões como try/finally ou Symbol.dispose.

É importante lembrar que o uso excessivo dessas APIs pode prejudicar o desempenho, pois o GC precisa rastrear as referências fracas e executar callbacks. Use com moderação e sempre documente a intenção.

Exemplos

Vamos explorar exemplos práticos que ilustram o uso de WeakRef e FinalizationRegistry.

Exemplo 1: Cache com WeakRef

Um cache simples que armazena valores calculados, mas permite que sejam coletados se não houver outras referências.

function createCache() {
  const cache = new Map(); // Mapa de chave -> WeakRef

  return {
    get(key) {
      const ref = cache.get(key);
      if (ref) {
        const obj = ref.deref();
        if (obj !== undefined) {
          return obj;
        }
        // Se undefined, o objeto foi coletado, removemos a entrada
        cache.delete(key);
      }
      return undefined;
    },
    set(key, value) {
      // Se o valor é um objeto (não primitivo), podemos usar WeakRef
      if (value !== null && typeof value === 'object') {
        cache.set(key, new WeakRef(value));
      } else {
        // Para primitivos, armazenamos diretamente (não faz sentido WeakRef)
        cache.set(key, { deref: () => value }); // hack para manter interface
      }
    }
  };
}

const cache = createCache();
let obj = { dados: 'importante' };
cache.set('obj', obj);
console.log(cache.get('obj')); // { dados: 'importante' }

// Removendo a referência forte
obj = null;
// Em algum momento, o GC pode coletar o objeto
// cache.get('obj') pode retornar undefined

Exemplo 2: FinalizationRegistry para limpeza de recursos

Suponha que temos uma classe que representa uma conexão com um recurso externo, e queremos fechar a conexão quando o objeto for coletado.

const conexoes = new FinalizationRegistry((identificador) => {
  console.log(`Fechando conexão ${identificador}`);
  // Simula fechamento de conexão
});

class Conexao {
  constructor(identificador) {
    this.id = identificador;
    // Registra o objeto para limpeza
    conexoes.register(this, this.id);
  }

  // Método para fechar manualmente
  fechar() {
    console.log(`Conexão ${this.id} fechada manualmente`);
    // Remove o registro para evitar callback duplicado
    conexoes.unregister(this);
  }
}

let conn = new Conexao('abc');
conn.fechar(); // Fecha manualmente
conn = null; // Agora pode ser coletado, mas o callback não será chamado porque foi desregistrado

let conn2 = new Conexao('def');
conn2 = null; // Quando o GC coletar, chamará o callback: "Fechando conexão def"

Exemplo 3: Monitoramento de memória

Podemos usar FinalizationRegistry para detectar vazamentos de memória em aplicações, registrando objetos que esperamos que sejam coletados.

const monitor = new FinalizationRegistry((nome) => {
  console.log(`Objeto ${nome} foi coletado`);
});

function criarObjetoTemporario() {
  const obj = { nome: 'temp' };
  monitor.register(obj, obj.nome);
  return obj;
}

let temp = criarObjetoTemporario();
temp = null; // Agora é elegível para coleta

Esses exemplos mostram como usar as APIs de forma prática. Lembre-se de que o comportamento real depende do GC, então os logs podem não aparecer imediatamente ou até não aparecer em execuções curtas.

Boas Práticas e Observações Finais

Ao trabalhar com WeakRef e FinalizationRegistry, siga estas recomendações:

  • Nunca dependa do timing exato do GC. Use essas APIs apenas para otimizações ou limpeza de recursos não críticos.
  • Sempre chame unregister quando não precisar mais do callback. Isso evita que callbacks sejam executados após a limpeza manual.
  • Use deref() com cuidado. Sempre verifique se o retorno não é undefined antes de usar.
  • Prefira padrões explícitos de limpeza (como try/finally ou Symbol.dispose) para recursos críticos, como conexões de rede ou arquivos.
  • Documente o uso dessas APIs em seu código, pois elas são pouco conhecidas e podem confundir outros desenvolvedores.

Em resumo, WeakRef e FinalizationRegistry são ferramentas poderosas para casos específicos, mas devem ser usadas com cautela. Entender o GC é fundamental para aproveitar essas APIs sem introduzir bugs sutis.

Referências

Exercícios

  1. Crie uma função que recebe um objeto e retorna uma WeakRef para ele. Teste se deref() retorna o objeto antes de remover a referência forte.

    ✓ Resposta:
    function criarWeakRef(obj) {
      return new WeakRef(obj);
    }
    
    const obj = { nome: 'teste' };
    const ref = criarWeakRef(obj);
    console.log(ref.deref() === obj); // true
    obj = null;
    // Em algum momento, ref.deref() pode ser undefined
  2. Escreva uma classe que use FinalizationRegistry para registrar cada instância e, no callback, exiba uma mensagem com um identificador único.

    ✓ Resposta:
    const registry = new FinalizationRegistry((id) => {
      console.log(`Instância com id ${id} foi coletada`);
    });
    
    class MinhaClasse {
      constructor(id) {
        this.id = id;
        registry.register(this, this.id);
      }
    }
    
    let instancia = new MinhaClasse('x1');
    instancia = null; // Quando o GC rodar, verá a mensagem
  3. Implemente um cache que usa WeakRef para armazenar valores, mas que também lida com primitivos (armazenando-os diretamente).

    ✓ Resposta:
    function createCache() {
      const cache = new Map();
    
      return {
        get(key) {
          const ref = cache.get(key);
          if (ref) {
            const val = ref.deref();
            if (val !== undefined) return val;
            cache.delete(key);
          }
          return undefined;
        },
        set(key, value) {
          if (value !== null && typeof value === 'object') {
            cache.set(key, new WeakRef(value));
          } else {
            // Para primitivos, armazenamos um objeto com deref
            cache.set(key, { deref: () => value });
          }
        }
      };
    }
    
    const cache = createCache();
    let ob = { a: 1 };
    cache.set('obj', ob);
    console.log(cache.get('obj')); // { a: 1 }
    ob = null;
    // Pode retornar undefined depois
    
    cache.set('num', 42);
    console.log(cache.get('num')); // 42
  4. Explique por que não é seguro usar FinalizationRegistry para fechar conexões de banco de dados e sugira uma alternativa.

    ✓ Resposta: Não é seguro porque o callback pode não ser executado em tempo hábil, ou até nunca, dependendo do GC. Isso pode esgotar conexões. Alternativa: usar um padrão de gerenciamento explícito, como try/finally ou uma classe com método close() chamado manualmente, ou ainda usar Symbol.dispose (proposta) para integração com using.
  5. Crie um exemplo que demonstre o uso de unregister em um FinalizationRegistry para evitar callbacks duplicados após limpeza manual.

    ✓ Resposta:
    const registry = new FinalizationRegistry((held) => {
      console.log(`Coletado: ${held}`);
    });
    
    let obj = { nome: 'alvo' };
    registry.register(obj, obj.nome);
    
    // Simula limpeza manual
    registry.unregister(obj);
    obj = null;
    // O callback NÃO será chamado, pois foi desregistrado