Поиск утечек памяти
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. Поэтапный фикс
- Зафиксируй baseline memory
- Фикс одну suspected утечку
- Re-измерь — стало лучше?
- Если нет — возможно, не та
- Итерируй
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 в
useEffectreturn - Lifecycle awareness в OOP
- Memory regression тесты
В конце
- Подтверждение leak (графики)
- Root cause (найденный объект и почему держится)
- Fix (точечный)
- Regression test для не возвращения
Аудит производительности (Core Web Vitals)
Глубокая проверка LCP, INP, CLS с привязкой к коду и приоритизированным планом исправлений.
Мастер-аудит сайта: 6 измерений за один проход
Orchestrator-аудит по 6 направлениям: UX, accessibility, performance, SEO, brand consistency, security. Quick scan + deep dive + приоритизированный план + композитная оценка + roadmap.
Performance budget по типам страниц
Бюджеты JS/CSS/images для разных типов страниц, целевые Web Vitals, enforcement в CI с конкретными порогами.