Skip to main content

Профілювання використання пам'яті

Профілювання пам'яті, це вимірювання того, скільки 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, незняті слухачі подій, живі таймери, підписки без відписки, кеші без обмеження розміру.

Швидкий приклад

javascript
// 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».

  1. Відкрийте DevTools, вкладка Memory.
  2. Виберіть Heap snapshot і натисніть Take snapshot (знімок 1).
  3. Виконайте підозрілі дії: навігація по SPA, відкриття та закриття модалок, списків тощо.
  4. Натисніть Take snapshot ще раз (знімок 2).
  5. Порівняйте: режим Comparison, сортування за Delta Size або Delta Count. Шукайте власників, що зростають (класи та конструктори), Detached DOM nodes, великі масиви і Map.

На що дивитися у знімку:

  • Retainers, ланцюги утримання: показують, яке саме посилання заважає GC звільнити об'єкт.
  • Distance: мале значення означає близькість до кореня (root), такі об'єкти звільняються рідко.

2) Реальний час: де саме створюються об'єкти.

Режим Allocation instrumentation on timeline:

  1. Memory, далі Allocation instrumentation on timeline, далі Start.
  2. Відтворіть сценарій протягом 10-30 секунд.
  3. Stop, клікайте по піках графіка і дивіться Allocation stacks, тобто рядки коду, де відбуваються аллокації.

Плюс: видно гарячі точки виділень прямо по місцю в коді. Мінус: багато шуму, але для ловлі сплесків це найкращий режим.

3) Легкий режим.

Sampling allocation profiler менш точний, зате майже без оверхеду. Зручний, коли баг важко відтворити швидко і профіль треба тримати довго.

4) Таймлайн пам'яті.

У вкладці Performance увімкніть метрику Memory і зробіть запис. Якщо після ручного GC (іконка кошика) heap не повертається приблизно до початкового рівня, є витік.

5) Швидкі перевірки з консолі.

javascript
// Поточний розмір heap (лише Chromium): performance.memory.usedJSHeapSize; // Експериментальний, детальніший API: if (performance.measureUserAgentSpecificMemory) { performance.measureUserAgentSpecificMemory().then(console.log); }

Профілювання в Node.js

1) Інспекція через DevTools.

javascript
node --inspect index.js # або із зупинкою на першому рядку node --inspect-brk index.js

Далі у Chrome відкрийте chrome://inspect, натисніть Open dedicated DevTools і працюйте у вкладці Memory так само, як у браузері.

2) Знімок heap із рантайму.

javascript
npm i heapdump
javascript
import heapdump from 'heapdump'; setInterval(() => { heapdump.writeSnapshot(`./heap-${Date.now()}.heapsnapshot`); }, 60_000);

Відкрийте файли .heapsnapshot у DevTools і порівняйте їх між собою.

3) Швидкі лічильники.

process.memoryUsage() кожні кілька секунд, як у прикладі вище. Якщо heapUsed пиляє вгору і не повертається, це підозра на витік.

4) GC-трейси для діагностики.

javascript
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: протокол перевірки

  1. Відкрийте сторінку A, зробіть Heap snapshot 1.
  2. Перейдіть на сторінку B і поверніться на A (або відкрийте та закрийте компонент), зробіть знімок 2.
  3. Натисніть GC (іконка кошика), зробіть знімок 3.
  4. Порівняйте знімки 1 і 3. Якщо об'єкти сторінки A або компонента залишилися, а їхня кількість чи розмір зростають після повторів, це витік.

Що шукати:

  • Detached DOM nodes;
  • обробники EventListener без removeEventListener;
  • активні setTimeout і setInterval;
  • «зомбі»-підписки (WebSocket, RxJS);
  • кеші на Map чи Set без TTL і без очищення;
  • великі структури в Context або Redux, які ніхто не скидає.

React-специфіка:

  • перевіряйте useEffect на cleanup, тобто на повернену функцію:

    javascript
    useEffect(() => { const id = setInterval(fetchData, 5000); return () => clearInterval(id); // обов'язково! }, []);
  • React DevTools Profiler: компонент має отримати Unmount, коли ви йдете зі сторінки;

  • стежте за ref на DOM: після unmount вони мають стати null.

Методика пошуку витоку

  1. Зафіксуйте симптом: «за N хвилин heap росте з X до Y».
  2. Звузьте сценарій до мінімально відтворюваної послідовності дій.
  3. Зберіть дані: heap snapshot до і після, Allocation timeline, запис Performance з метрикою Memory.
  4. Знайдіть власника: клас або конструктор, за яким іде зростання, далі дивіться Retainers.
  5. Зв'яжіть з кодом: файл і рядок через Allocation stacks або source maps.
  6. Виправте: додайте cleanup, приберіть зайві посилання, обмежте кеш.
  7. Верифікуйте: повторіть кроки 1-3, зростання має зникнути.
  8. Автоматизуйте, якщо можливо: 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-вузли у ваш звіт.
  • Шукати витік без мінімального сценарію: шум від сторонніх дій буде сильнішим за сам витік.
  • Прибрати симптом і не перевірити фікс повторним профілюванням.

Коротка відповідь

Для співбесіди
Premium

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