WeakRef y FinalizationRegistry en JavaScript: referencias débiles y gestión de memoria

El recolector de basura de JavaScript libera objetos cuando no quedan referencias a ellos. Esto funciona bien en general, pero hay casos donde mantener una referencia fuerte impide liberar memoria que ya no necesitas: cachés, registros de objetos del DOM, listeners vinculados a instancias. WeakRef y FinalizationRegistry, disponibles desde ES2021, resuelven estos casos concretos.

WeakRef: referencia que no retiene al GC

Una WeakRef guarda una referencia a un objeto sin evitar que el recolector de basura lo elimine. Para acceder al objeto se usa .deref(), que devuelve el objeto si sigue en memoria o undefined si ya fue recolectado.

let elemento = document.getElementById('widget');
const ref = new WeakRef(elemento);

// Más tarde, en otro punto del código:
function procesarElemento() {
  const el = ref.deref();
  if (!el) {
    console.log('El elemento ya no existe en el DOM ni en memoria');
    return;
  }
  el.classList.add('procesado');
}

El objeto puede ser recolectado en cualquier momento en que el motor decida hacerlo, así que siempre debes comprobar si deref() devuelve algo antes de usarlo.

Caché que no retiene memoria

Una caché con referencias fuertes retiene todos sus objetos aunque nada más los use. Con WeakRef la caché puede crecer sin retener memoria indefinidamente:

class CacheDebil {
  #mapa = new Map();

  get(clave) {
    const ref = this.#mapa.get(clave);
    if (!ref) return undefined;
    const valor = ref.deref();
    if (valor === undefined) {
      this.#mapa.delete(clave); // limpieza proactiva
    }
    return valor;
  }

  set(clave, valor) {
    this.#mapa.set(clave, new WeakRef(valor));
  }

  has(clave) {
    return this.get(clave) !== undefined;
  }
}

const cache = new CacheDebil();
let usuario = { id: 1, nombre: 'Ana' };
cache.set('usuario-1', usuario);

console.log(cache.get('usuario-1')?.nombre); // "Ana"

// Si el GC recoge al objeto (cuando se pierdan todas las refs fuertes):
// cache.get('usuario-1') ? undefined

FinalizationRegistry: callback al liberar un objeto

FinalizationRegistry permite registrar un callback que se ejecuta cuando un objeto es recolectado por el GC. El callback recibe un valor de limpieza que defines al registrar el objeto.

const registro = new FinalizationRegistry((clave) => {
  console.log(`Objeto con clave "${clave}" fue recolectado`);
  // Aquí puedes liberar recursos externos, cerrar conexiones, etc.
});

function crearConexion(id) {
  const conexion = { id, activa: true };
  registro.register(conexion, `conexion-${id}`);
  return conexion;
}

let conn = crearConexion(42);
// Cuando conn pierda todas sus referencias fuertes y el GC la recoja:
// ? "Objeto con clave "conexion-42" fue recolectado"

Registro de objetos del DOM con limpieza automática

Una combinación de WeakRef y FinalizationRegistry permite mantener un registro de elementos del DOM que se limpia solo cuando los elementos se eliminan:

const registro = new FinalizationRegistry((id) => {
  console.log(`Elemento ${id} eliminado del DOM y de memoria`);
});

const elementos = new Map();

function rastrearElemento(el) {
  const id = el.dataset.id;
  elementos.set(id, new WeakRef(el));
  registro.register(el, id);
}

function obtenerElemento(id) {
  return elementos.get(id)?.deref();
}

Consideraciones importantes

Hay varios puntos a tener en cuenta antes de usar estas APIs. El GC no es determinista: no puedes predecir cuándo o si recolectará un objeto concreto. No uses WeakRef como sustituto de lógica de ciclo de vida explícita. El callback de FinalizationRegistry puede ejecutarse en cualquier momento (incluso mucho después de que el objeto sea elegible para recolección) o incluso nunca si el programa termina antes. Estas APIs son una herramienta de último recurso para optimizaciones de memoria, no un mecanismo de control de flujo general.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP