В асинхронных пайплайнах — от Node.js-сервисов до SSR и heavy client-side кэширования — каждый временный объект (кэши, буферы, промисы, слушатели) держится сильной ссылкой, блокируя сборщик мусора. Типичная ошибка: держать в Map результаты дорогих API-вызовов, не понимая, что Map хранит сильные ссылки, и память растёт бесконтрольно.
WeakRef: слабая ссылка для кэширования без утечек
WeakRef позволяет GC очищать объект, если на него нет сильных ссылок. Идеально для кэша, где данные можно пересоздать — экономит память под нагрузкой:
const cache = new Map<string, WeakRef<any>>();
const registry = new FinalizationRegistry((key: string) => {
cache.delete(key);
});
async function fetchData(key: string): Promise<any> {
let ref = cache.get(key);
let cached = ref?.deref();
if (cached) return cached;
const data = await expensiveAPICall(key);
const weakRef = new WeakRef(data);
cache.set(key, weakRef);
registry.register(data, key);
return data;
}
GC решает, когда удалить объект. Если памяти хватает — данные живут. Если нет — кэш "сдувается" без ручной очистки и утечек.
FinalizationRegistry: детекция утечек и чистка ресурсов
Колбэк при удалении объекта — можно отслеживать, что объект не был освобождён вовремя:
class LeakDetector {
private registry: FinalizationRegistry<string>;
private active = new Map<string, number>();
constructor(onLeak: (name: string) => void) {
this.registry = new FinalizationRegistry((name: string) => {
const count = (this.active.get(name) ?? 0) - 1;
if (count <= 0) this.active.delete(name);
else this.active.set(name, count);
onLeak(name);
});
}
track(obj: object, name: string) {
this.active.set(name, (this.active.get(name) ?? 0) + 1);
this.registry.register(obj, name);
}
}Практический совет: используй в отладке для обнаружения утечек долгоживущих объектов — подписок, web-сокетов, буферов.
Типобезопасная обёртка с generic
TypeScript дружит с WeakRef через generic, что даёт надёжные типы:
class WeakCache<T extends object> {
private refs = new Map<string, WeakRef<T>>();
private registry = new FinalizationRegistry<string>((key) => {
this.refs.delete(key);
});
set(key: string, value: T): void {
const ref = new WeakRef(value);
this.refs.set(key, ref);
this.registry.register(value, key);
}
get(key: string): T | undefined {
return this.refs.get(key)?.deref();
}
}Предупреждение: WeakRef не работают с примитивами (string, number) — только объекты. Для примитивов используй
WeakMap или обёртки. Также не используй WeakRef для критичных по времени кэшей — GC непредсказуем.Вывод: Слабые ссылки — не экзотика, а инженерный инструмент для надёжного управления памятью, где главное правило — "используй, если данные можно пересоздать, и никогда не полагайся на детерминизм GC".