Профілювання використання пам'яті
Профілювання пам'яті, це вимірювання того, скільки heap займає застосунок, які об'єкти в ньому живуть і які посилання заважають збирачу сміття їх звільнити. У браузері це робиться вкладкою Memory у Chrome DevTools, у Node.js, через інспектор, знімки heap і лічильники process.memoryUsage().
Теорія
TL;DR
- Базовий прийом: зняти heap snapshot до сценарію, повторити сценарій, зняти знімок після і порівняти їх у режимі Comparison за Delta Size та Delta Count.
- Колонка Retainers показує ланцюг посилань, який утримує об'єкт; Distance показує, наскільки близько об'єкт до кореня.
- Allocation instrumentation on timeline дає стеки аллокацій з рядками коду, Sampling allocation profiler дешевший, але приблизний.
- У Node.js доступні
node --inspect, знімки heap у DevTools,heapdump,process.memoryUsage()іnode --trace-gc. - Ознака витоку: після примусового GC heap не повертається до початкового рівня.
- Найчастіші винуватці: detached DOM nodes, незняті слухачі подій, живі таймери, підписки без відписки, кеші без обмеження розміру.
Швидкий приклад
// Node.js: найдешевший спосіб побачити тренд пам'яті
setInterval(() => {
const m = process.memoryUsage();
const mb = (v) => Math.round(v / 1024 / 1024) + 'MB';
console.log({
rss: mb(m.rss), // уся резидентна пам'ять процесу
heapUsed: mb(m.heapUsed), // реально зайнято в JS heap
heapTotal: mb(m.heapTotal), // виділено під heap
ext: mb(m.external), // буфери поза heap
});
}, 5000);
// Якщо heapUsed монотонно пиляє вгору і не спадає, це привід
// знімати heap snapshot і шукати власника зростання.Профілювання в браузері (Chrome DevTools)
1) Базовий сценарій «чи росте heap».
- Відкрийте DevTools, вкладка Memory.
- Виберіть Heap snapshot і натисніть Take snapshot (знімок 1).
- Виконайте підозрілі дії: навігація по SPA, відкриття та закриття модалок, списків тощо.
- Натисніть Take snapshot ще раз (знімок 2).
- Порівняйте: режим Comparison, сортування за Delta Size або Delta Count. Шукайте власників, що зростають (класи та конструктори), Detached DOM nodes, великі масиви і
Map.
На що дивитися у знімку:
- Retainers, ланцюги утримання: показують, яке саме посилання заважає GC звільнити об'єкт.
- Distance: мале значення означає близькість до кореня (root), такі об'єкти звільняються рідко.
2) Реальний час: де саме створюються об'єкти.
Режим Allocation instrumentation on timeline:
- Memory, далі Allocation instrumentation on timeline, далі Start.
- Відтворіть сценарій протягом 10-30 секунд.
- Stop, клікайте по піках графіка і дивіться Allocation stacks, тобто рядки коду, де відбуваються аллокації.
Плюс: видно гарячі точки виділень прямо по місцю в коді. Мінус: багато шуму, але для ловлі сплесків це найкращий режим.
3) Легкий режим.
Sampling allocation profiler менш точний, зате майже без оверхеду. Зручний, коли баг важко відтворити швидко і профіль треба тримати довго.
4) Таймлайн пам'яті.
У вкладці Performance увімкніть метрику Memory і зробіть запис. Якщо після ручного GC (іконка кошика) heap не повертається приблизно до початкового рівня, є витік.
5) Швидкі перевірки з консолі.
// Поточний розмір heap (лише Chromium):
performance.memory.usedJSHeapSize;
// Експериментальний, детальніший API:
if (performance.measureUserAgentSpecificMemory) {
performance.measureUserAgentSpecificMemory().then(console.log);
}Профілювання в Node.js
1) Інспекція через DevTools.
node --inspect index.js
# або із зупинкою на першому рядку
node --inspect-brk index.jsДалі у Chrome відкрийте chrome://inspect, натисніть Open dedicated DevTools і працюйте у вкладці Memory так само, як у браузері.
2) Знімок heap із рантайму.
npm i heapdumpimport heapdump from 'heapdump';
setInterval(() => {
heapdump.writeSnapshot(`./heap-${Date.now()}.heapsnapshot`);
}, 60_000);Відкрийте файли .heapsnapshot у DevTools і порівняйте їх між собою.
3) Швидкі лічильники.
process.memoryUsage() кожні кілька секунд, як у прикладі вище. Якщо heapUsed пиляє вгору і не повертається, це підозра на витік.
4) GC-трейси для діагностики.
node --trace-gc index.jsДивіться частоту і тривалість пауз. Постійне зростання показника «after GC» це поганий знак: збирач працює, але звільнити нічого не може.
| Інструмент | Що дає | Оверхед |
|---|---|---|
| Heap snapshot | повний граф об'єктів, Retainers, порівняння знімків | високий |
| Allocation instrumentation on timeline | стеки аллокацій з рядками коду | високий |
| Sampling allocation profiler | приблизні гарячі точки виділень | низький |
| Performance, метрика Memory | форма графіка heap у часі, ефект ручного GC | середній |
process.memoryUsage() | rss, heapUsed, heapTotal, external | майже нульовий |
node --trace-gc | частота і тривалість пауз GC, рівень після GC | низький |
SPA і React: протокол перевірки
- Відкрийте сторінку A, зробіть Heap snapshot 1.
- Перейдіть на сторінку B і поверніться на A (або відкрийте та закрийте компонент), зробіть знімок 2.
- Натисніть GC (іконка кошика), зробіть знімок 3.
- Порівняйте знімки 1 і 3. Якщо об'єкти сторінки A або компонента залишилися, а їхня кількість чи розмір зростають після повторів, це витік.
Що шукати:
- Detached DOM nodes;
- обробники
EventListenerбезremoveEventListener; - активні
setTimeoutіsetInterval; - «зомбі»-підписки (WebSocket, RxJS);
- кеші на
MapчиSetбез TTL і без очищення; - великі структури в Context або Redux, які ніхто не скидає.
React-специфіка:
-
перевіряйте
useEffectна cleanup, тобто на повернену функцію:javascriptuseEffect(() => { const id = setInterval(fetchData, 5000); return () => clearInterval(id); // обов'язково! }, []); -
React DevTools Profiler: компонент має отримати Unmount, коли ви йдете зі сторінки;
-
стежте за
refна DOM: після unmount вони мають статиnull.
Методика пошуку витоку
- Зафіксуйте симптом: «за N хвилин heap росте з X до Y».
- Звузьте сценарій до мінімально відтворюваної послідовності дій.
- Зберіть дані: heap snapshot до і після, Allocation timeline, запис Performance з метрикою Memory.
- Знайдіть власника: клас або конструктор, за яким іде зростання, далі дивіться Retainers.
- Зв'яжіть з кодом: файл і рядок через Allocation stacks або source maps.
- Виправте: додайте cleanup, приберіть зайві посилання, обмежте кеш.
- Верифікуйте: повторіть кроки 1-3, зростання має зникнути.
- Автоматизуйте, якщо можливо: e2e-сценарій, який міряє
usedJSHeapSizeпокроково.
Часті підказки у звітах профайлера
- Росте Detached HTMLDivElement: забули зняти слухача або десь лишилося посилання на видалений DOM.
- Ростуть масиви та об'єкти всередині
MapчиSet: немає TTL або LRU-очищення кеша. - Багато записів
TimeoutіInterval: немаєclearTimeoutтаclearInterval. - Купа екземплярів компонента після навігації: компонент не розмонтовується або його тримають замикання і підписки.
- Зростання всередині сторонньої бібліотеки: оберніть її у власний адаптер із cleanup або замініть.
Типові помилки
- Робити один знімок і робити з нього висновки. Витік видно лише у порівнянні двох або трьох знімків.
- Не тиснути GC перед контрольним знімком: частина об'єктів ще жива просто тому, що збирач не встиг відпрацювати.
- Вважати витоком будь-яке зростання
heapUsed. V8 не збирає сміття, поки не треба, тому важливий рівень після GC, а не пік до нього. - Профілювати dev-збірку: source maps, HMR і DevTools самі тримають об'єкти й дають хибні detached-вузли.
- Покладатися на
performance.memory: це нестандартний API лише для Chromium, значення огрублені з міркувань безпеки. - Профілювати у вкладці з увімкненими розширеннями: вони додають власні аллокації та detached-вузли у ваш звіт.
- Шукати витік без мінімального сценарію: шум від сторонніх дій буде сильнішим за сам витік.
- Прибрати симптом і не перевірити фікс повторним профілюванням.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.