Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Профілювання використання пам'яті». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Пам'ять профілюють порівнянням heap snapshot до і після підозрілого сценарію, а не одним знімком.** У Chrome DevTools вкладка Memory дає три режими: Heap snapshot для порівняння станів, Allocation instrumentation on timeline для стеків аллокацій з рядками коду, Sampling allocation profiler для дешевого профілю без помітного оверхеду. Колонка Retainers у знімку показує ланцюг посилань, який тримає об'єкт і не дає його зібрати, а вкладка Performance з увімкненою метрикою Memory малює форму heap у часі. У Node.js той самий інструментарій доступний через `node --inspect` і chrome://inspect, плюс `heapdump` для знімка з рантайму, `process.memoryUsage()` для лічильників rss і heapUsed та `node --trace-gc` для пауз збирача. ```javascript setInterval(() => { const m = process.memoryUsage(); console.log(Math.round(m.heapUsed / 1024 / 1024) + 'MB'); }, 5000); ``` **Ключове:** витік доводить не саме зростання heap, а те, що після примусового GC він не повертається приблизно до початкового рівня.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Профілювання пам'яті, це вимірювання того, скільки 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-вузли у ваш звіт. - Шукати витік без мінімального сценарію: шум від сторонніх дій буде сильнішим за сам витік. - Прибрати симптом і не перевірити фікс повторним профілюванням.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.