Skip to main content

Як уникнути витоків пам'яті?

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().
  • Стріми: завжди .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

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.