WeakRef e FinalizationRegistry
LidoEsta aula explora as APIs WeakRef e FinalizationRegistry em JavaScript, que permitem interagir com o mecanismo de garbage collection. Você aprenderá o que são, quando usá-las (e quando evitá-las), e verá exemplos práticos, além de boas práticas e exercícios para fixar o conhecimento.
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
FinalizationRegistrynão garante que o callback será chamado antes do programa terminar. Para recursos críticos, use padrões comotry/finallyouSymbol.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
unregisterquando 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 éundefinedantes de usar. - Prefira padrões explícitos de limpeza (como
try/finallyouSymbol.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
- MDN: WeakRef
- MDN: FinalizationRegistry
- ECMAScript Specification: WeakRef Objects
- ECMAScript Specification: FinalizationRegistry Objects
- V8: WeakRefs and FinalizationRegistry
- web.dev: WeakRefs and FinalizationRegistry
Exercícios
Crie uma função que recebe um objeto e retorna uma
WeakRefpara ele. Teste sederef()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 undefinedEscreva uma classe que use
FinalizationRegistrypara 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 mensagemImplemente um cache que usa
WeakRefpara 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')); // 42Explique por que não é seguro usar
FinalizationRegistrypara 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, comotry/finallyou uma classe com métodoclose()chamado manualmente, ou ainda usarSymbol.dispose(proposta) para integração comusing.Crie um exemplo que demonstre o uso de
unregisterem umFinalizationRegistrypara 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