Skip to content
PПромтбук
RUEN
04Производительность

Поиск утечек памяти

Heap-snapshot, retained size, типы утечек, как не повторять.

Найди и исправь memory leak.

Симптомы

  • Память растёт со временем без падений
  • OOM crashes после N часов uptime
  • GC всё чаще, длиннее
  • Performance падает с течением времени

1. Подтверди что это leak

Не просто "память выросла" — может быть нормально (heap grows для cache).

Признаки именно leak:

  • Память растёт монотонно
  • Не падает после идеала / GC
  • Растёт пропорционально usage
Время:     0h    1h    2h    3h    4h
Memory:   200M  280M  360M  440M  520M  ← leak

vs normal:

Память: 200-300M, осциллирует

2. Воспроизведи в dev

  • Реплицируй production load (нагрузка, длительность)
  • Меньший масштаб (1/10) на dev машине
  • Профайлер прикреплён

3. Heap snapshot

Node.js

node --inspect app.js

Open Chrome DevTools → Memory tab → Take heap snapshot

Сделай 3 снимка с интервалом — что росло между ними?

Browser

DevTools → Memory → Heap snapshot → Comparison mode

4. Анализ snapshot

Сортируй по Retained Size (не Shallow).

Ищи:

  • Объекты с растущим count'ом
  • Цепочка retainers (что держит ссылку?)
  • Detached DOM nodes (frontend)

5. Распространённые причины

A. Listeners не отписываются

// плохо
useEffect(() => {
  window.addEventListener('scroll', handler);
}, []);

// хорошо
useEffect(() => {
  window.addEventListener('scroll', handler);
  return () => window.removeEventListener('scroll', handler);
}, []);

B. Closures держат большие объекты

function createHandler(hugeData) {
  return () => console.log('clicked');
}
// hugeData в closure, даже если не используется

C. Глобальные cache без bounds

// плохо
const cache = {};
function memoize(key, fn) {
  cache[key] = fn();  // ← растёт forever
}

// хорошо
const cache = new LRU({ max: 1000 });

D. Timers не очищаются

// плохо
setInterval(() => doStuff(), 1000);

// хорошо
const handle = setInterval(() => doStuff(), 1000);
// cleanup: clearInterval(handle);

E. Detached DOM (frontend)

Component unmounted, но reference на его DOM в JS остался.

F. Циклические ссылки

A → B → A → cycle. Modern GC handles это, но иногда нет.

G. Subscribers/observers

Subscribers в EventEmitter без unsubscribe.

6. Поэтапный фикс

  1. Зафиксируй baseline memory
  2. Фикс одну suspected утечку
  3. Re-измерь — стало лучше?
  4. Если нет — возможно, не та
  5. Итерируй

7. Регрессия-тест

После фикса:

  • Long-running test (несколько часов) — память стабильна?
  • Memory baseline в CI (alert если растёт)

Node-specific

# Max heap size (по умолчанию 1.5-4GB на 64-bit)
node --max-old-space-size=4096 app.js

# Принудительный GC между requests (для dev)
node --expose-gc

# В коде:
if (global.gc) global.gc();

Browser-specific

  • Performance.measureUserAgentSpecificMemory() — точный размер
  • WeakRef / WeakMap для cache которые могут быть очищены GC
  • Performance Observer для long tasks

Анти-паттерны

  • ❌ "Подкрутить max-old-space-size" вместо фикса
  • ❌ Restart сервиса каждые N часов (workaround не fix)
  • ❌ Удалить cache "на всякий случай"
  • ❌ Не воспроизвести — фикс наугад

Превентивные меры

  • Лимиты на все cache
  • Cleanup в useEffect return
  • Lifecycle awareness в OOP
  • Memory regression тесты

В конце

  • Подтверждение leak (графики)
  • Root cause (найденный объект и почему держится)
  • Fix (точечный)
  • Regression test для не возвращения
Похожие промты