WeakRef и FinalizationRegistry

WeakRef — слабая ссылка на объект, не препятствующая его сборке GC; FinalizationRegistry — механизм регистрации callback, вызываемого когда объект был собран сборщиком мусора (ES2021).

Зачем нужно

WeakRef решает задачи, где нужен доступ к объекту, но без удержания в памяти — например, кэш, который должен «протухать» при нехватке памяти. FinalizationRegistry позволяет выполнить cleanup-операции после удаления объекта (например, освободить внешний ресурс). Оба API низкоуровневые и нужны редко.

Где используется

  • Реализация кэша, который не мешает GC
  • Освобождение внешних ресурсов (WebGL текстуры, файловые дескрипторы)
  • Мониторинг утечек памяти
  • Ослабленные ссылки на DOM-элементы в сложных структурах данных

Основной контент

WeakRef

// WeakRef хранит слабую ссылку
let target = { data: 'Важные данные', size: 'large' };
const ref = new WeakRef(target);

// Получить объект: deref — вернёт объект или undefined
let obj = ref.deref;
console.log(obj?.data); // 'Важные данные'

// Пока target в памяти — deref работает
target = null; // убираем сильную ссылку

// После GC (момент непредсказуем):
// ref.deref → undefined

// ВАЖНО: всегда проверяйте результат deref
function processIfAlive(weakRef) {
  const obj = weakRef.deref;
  if (obj === undefined) {
    console.log('Объект был собран GC');
    return;
  }
  console.log('Обрабатываем:', obj.data);
}

Кэш с WeakRef

// Кэш, который «протухает» при нехватке памяти
class WeakCache {
  #cache = new Map();

  get(key) {
    const ref = this.#cache.get(key);
    if (!ref) return undefined;

    const value = ref.deref;
    if (value === undefined) {
      // GC собрал объект — чистим запись
      this.#cache.delete(key);
      return undefined;
    }
    return value;
  }

  set(key, value) {
    this.#cache.set(key, new WeakRef(value));
  }

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

const cache = new WeakCache();
let bigObject = { data: new Array(100000).fill('x') };
cache.set('big', bigObject);

console.log(cache.has('big')); // true
bigObject = null;
// После GC: cache.has('big') → false

FinalizationRegistry

// Регистрируем callback для cleanup после сборки объекта
const registry = new FinalizationRegistry((heldValue) => {
  // heldValue — значение, переданное при регистрации (не объект!)
  console.log(`Объект "${heldValue}" был удалён GC`);
  // Cleanup: освободить внешние ресурсы
});

function createManagedResource(name) {
  const resource = { name, data: new Array(10000).fill(0) };

  // Регистрируем: когда resource будет собран — вызовем callback
  registry.register(resource, name); // resource — объект, name — held value

  return resource;
}

let res = createManagedResource('texture-1');
res = null;
// Когда GC соберёт res: 'Объект "texture-1" был удалён GC'

WeakRef + FinalizationRegistry вместе

class ResourceManager {
  #refs = new Map();
  #registry = new FinalizationRegistry((id) => {
    console.log(`Ресурс ${id} освобождён`);
    this.#refs.delete(id);
  });

  add(id, resource) {
    this.#refs.set(id, new WeakRef(resource));
    this.#registry.register(resource, id);
  }

  get(id) {
    return this.#refs.get(id)?.deref;
  }
}

Частые ошибки

  • Полагаться на немедленную сборку — GC работает непредсказуемо. Не строй логику на предположении, что объект будет собран в конкретный момент.
  • Передавать сам объект как heldValue в FinalizationRegistryregistry.register(obj, obj) создаёт сильную ссылку через heldValue — obj никогда не будет собран!
  • WeakRef вместо WeakMap — если нужно хранить данные, связанные с объектом, WeakMap проще и надёжнее WeakRef + кэш. WeakRef нужен в специфических сценариях.

Связанные темы

Ресурсы