Як уникнути витоків пам'яті?
1) Базові принципи
- Немає посилання - немає витоку. Все, що залишається досяжним з коренів (глобальні змінні, замикання, DOM, кеші), не збереться сміттєзбірником.
- Скорочуйте час життя об'єктів і вчасно розривайте зв'язки (обробники, таймери, кеші, посилання на DOM).
2) Типові джерела витоків і як їх закрити
Обробники подій / підписки
Проблема: висячі слухачі тримають DOM/дані.
javascript
const onClick = () => {};
button.addEventListener('click', onClick);
// ...
button.removeEventListener('click', onClick); // обов'язково тим самим колбеком- У React: завжди робіть cleanup у
useEffect.
javascript
useEffect(() => {
const onScroll = () => {};
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll); //
}, []);- У Nest/EventEmitter: знімаємо підписки/відписуємось у
onModuleDestroy().
Таймери та інтервали
javascript
const id = setInterval(tick, 1000);
// ...
clearInterval(id); //- Node 20+: використовуйте AbortSignal для таймерів:
javascript
const ac = new AbortController();
setTimeout(cb, 5000, { signal: ac.signal });
// ...
ac.abort(); //Незавершені fetch/стріми
- Скасовуйте через
AbortController, закривайте стріми.
javascript
const c = new AbortController();
const res = await fetch(url, { signal: c.signal });
// ...
c.abort(); //"Відірвані" DOM-вузли
- Не зберігайте посилання на видалені з DOM ноди.
- Обнуляйте кеші з DOM після
remove():
javascript
elem.remove();
elemRef = null; // даємо GC шансВеликі замикання
- Не замикайте великі об'єкти без потреби. Передавайте мінімум у колбеки.
- Розривайте посилання після використання:
javascript
let big = getHugeObject();
doWork(() => { /* використовуємо small part */ });
big = null; //Глобальні синглтони/кеші
- Все, що в модулі/глобалі, живе "вічно". Обмежуйте розмір, додавайте очищення або слабкі посилання.
3) Кеші без витоків
- Ключі-об'єкти → WeakMap/WeakSet (не заважають GC):
javascript
const memo = new WeakMap();
function compute(obj) {
if (memo.has(obj)) return memo.get(obj);
const val = heavy(obj);
memo.set(obj, val);
return val;
}- Для рядків/чисел використовуйте LRU з обмеженням:
javascript
class LRU {
constructor(limit=500){ this.limit=limit; this.map=new Map(); }
get(k){ const v=this.map.get(k); if(v){ this.map.delete(k); this.map.set(k,v); } return v; }
set(k,v){
if(this.map.has(k)) this.map.delete(k);
this.map.set(k,v);
if(this.map.size>this.limit){
const oldest = this.map.keys().next().value;
this.map.delete(oldest); // витісняємо
}
}
}4) React / Next.js
- Cleanup усіх побічних ефектів (
useEffect→return () => {...}). - Не зберігайте величезні об'єкти в
useRef/контексті без потреби. - Уникайте нескінченних мікрозадач (ланцюжки
Promise), вони затримують GC і рендер. - Відписуйтесь від Observable/WebSocket у cleanup.
- Для великих списків - віртуалізація (менше об'єктів одночасно).
5) Node.js / NestJS
- Закривайте з'єднання/пули:
- Prisma:
await prisma.$disconnect()при завершенні. - Redis/Mongo/pg:
.quit()/.close()/.end()уonModuleDestroy().
- Prisma:
- Стріми: завжди
.destroy()/.end()при помилках і таймаутах. - EventEmitter:
emitter.removeListener/removeAllListenersпри вивантаженні. - Уникайте безконтрольних
setInterval; використовуйтеsetTimeout+ цикл з перевірками/abort. - Обробляйте сигнали:
javascript
process.on('SIGTERM', async () => {
await app.close(); // закрийте сервер/БД/черги
process.exit(0);
});6) Асинхронність і черги
- Встановлюйте таймаути і ліміти на паралелізм (p-limit, семафори).
- На мережах/стрімах - backpressure: не читайте швидше, ніж обробляєте.
- Не тримайте великі масиви промісів у пам'яті; обробляйте чанками.
7) FinalizationRegistry - лише для м'якого прибирання
- Підходить для зовнішніх кешів/хендлів, але не покладайтесь на порядок/час виклику.
- Бізнес-логіку на фіналізаторах не будуємо.
8) Інструменти пошуку витоків
Браузер (Chrome)
- Memory → Heap snapshot: шукайте "Detached HTML elements", ретейнери.
- Allocation instrumentation: хто створює і утримує.
- Performance: довгі мікрозадачі/часті GC - ознака проблеми.
Node
--inspect+ Chrome DevTools → Heap snapshots.heapdump/clinicдля знімків у бойовому середовищі.- Логи GC:
node --trace-gc app.js(для діагностики, не в prod). - Метрики: моніторте RSS/heapUsed, алерти на тренд зростання.
9) Гардрейли в коді і CI
- Ліміти на розміри кешів/черг.
- Е2Е-тест "прогріли N разів → пам'ять стабілізувалась".
- Правила лінтера: заборона анонімних слухачів там, де потрібне зняття, заборона
setIntervalбез явного clear.
Коротка відповідь
Для співбесідиPremium
Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.