Long-running процессы в Node.js — зона риска утечек памяти. ES2021 даёт два API для активного контроля над GC:
WeakRef и FinalizationRegistry. Многие разработчики полагаются на автоматическое управление памятью, не учитывая, что GC не может освободить ресурсы, удерживаемые сильными ссылками, а сами API недетерминированы.WeakRef: слабые ссылки
WeakRef не удерживает объект в памяти. GC может удалить его, если нет сильных ссылок. Это полезно для кэшей, где не требуется гарантированное хранение данных.
const cache = new Map();
function getOrCreate(key) {
const ref = cache.get(key);
if (ref) {
const obj = ref.deref();
if (obj) return obj;
cache.delete(key);
}
const obj = new ExpensiveObject();
cache.set(key, new WeakRef(obj));
return obj;
}
deref() может вернуть undefined в любой момент — GC асинхронен. Если подряд дёргаешь deref(), не жди, что вернёт то же самое. Типичная ошибка — полагаться на deref() как на гарантированный доступ.FinalizationRegistry: колбэк после GC
Вызывается после сборки объекта. Идеален для очистки нативных ресурсов, например, закрытия файловых дескрипторов.
const registry = new FinalizationRegistry((handle) => {
cleanupNativeHandle(handle);
});
function createResource() {
const handle = openNativeConnection();
registry.register({ handle }, handle);
return { handle };
}Предостережения: колбэк не гарантирует порядок и может не выполниться при падении процесса. В продакшене я бы не вешал на него критичную логику вроде записи в БД. Только освобождение нативных хендлов или логгирование.
Паттерн: MemoryGuard
Триггер на основе FinalizationRegistry для мониторинга памяти. Помогает выявить растущие объекты, но не заменяет ручное управление.
class MemoryGuard {
constructor(thresholdMB) {
this.threshold = thresholdMB;
this.registry = new FinalizationRegistry(() => this.onGC());
}
track(obj) { this.registry.register(obj, () => {}); }
onGC() {
const usage = process.memoryUsage().heapUsed / 1024 / 1024;
if (usage > this.threshold) console.warn(Memory spike: ${usage.toFixed(2)} MB);
}
}По опыту, этот паттерн хорош скорее для локальной отладки, чем для прода. В production лучше комбинировать с метриками в Prometheus и лимитами heap (
--max-old-space-size=4096).Production-паттерны
* LRU-кэш с WeakRef — автоочистка при нехватке памяти, но без гарантий времени.
* async_hooks для мониторинга асинхронных ресурсов: ловит утечки через незакрытые сокеты или таймеры.
* Регулярные snapshot
console.profile() для поиска растущих объектов.* Слушать
process.on('warning') для уведомлений о превышении лимитов.Я часто вижу, как разработчики забывают про async_hooks: он ловит утечки через незакрытые сокеты или таймеры. Это дешевле, чем дебажить heap snapshot.
Вывод: WeakRef и FinalizationRegistry — полезные инструменты для оптимизации памяти, но их недетерминированность требует ручного управления ссылками (WeakMap, cancellable promises) для production-надёжности.