Зміст
Коротко
У сучасному JavaScript рідко виділяють пам’ять вручну: за прибирання відповідає збирач сміття.
Витоки здебільшого з’являються не тому, що «забули звільнити», а тому що об’єкт лишається досяжним довше, ніж потрібно.
Вихідна стаття розбирає механіку купи V8, сім частих сценаріїв витоків і маршрут пошуку через Chrome DevTools: спочатку сигнал на графіку пам’яті, потім знімок купи й ланцюжок утримання.
Що сталося
Автор починає з простого правила рушія: об’єкт живе, поки від кореня збирача до нього тягнеться ланцюжок посилань.
Коренями служать глобальний об’єкт, стек викликів і активні системні дескриптори. Поки шлях від кореня існує, дані лишаються в купі.
Уявіть орендовану квартиру, з якої ви вже виїхали, але ключ досі в знайомого: поки ключ не здано, приміщення формально зайняте. Так само з зайвим посиланням у коді.
V8 ділить об’єкти за віком. Молоде покоління чиститься часто й швидко: більшість виділень короткоживучі.
Те, що пережило кілька проходів, переїжджає в старе покоління. Там працюють важчі цикли розмітки й ущільнення — зокрема паралельно й частинами, щоб рідше зупиняти весь світ.
Покоління змінює вартість прибирання, але не скасовує правило досяжності: зайве посилання від кореня — і об’єкт лишається в купі.
Далі — сім типових сценаріїв. Неоголошена змінна в нестрогому режимі «протікає» в глобальну область: функція завершилася, а великий масив досі висить на window.
Вузол прибрали з DOM через .remove() чи .removeChild(), але масив або кеш тримає його — виходить від’єднане дерево. У прикладі автора в вузла дрібний «власний» розмір, а утриманий розмір роздуває майже всю купу через масив на мільйон елементів.
Слухач на window чи document без removeEventListener зберігає і колбек, і все, що він замкнув. Компонент уже розмонтовано, а resize досі тягне важкі метадані.
Забутий setInterval або нескінченний requestAnimationFrame роблять те саме через таймери браузера. Колбек лишається досяжним, доки не викликати clearInterval чи cancelAnimationFrame.
Довгоживуче замикання тягне за собою гігантський масив, хоча потрібен був лише один розмір: достатньо зберегти число, а не весь масив.
Звичайний Map як безмежний кеш за об’єктами не відпускає ключі, доки не викликати .delete(). Тут доречні WeakMap / WeakSet, якщо ключі — об’єкти й ітерація за записами не потрібна.
Нарешті, console.log великих структур у відкритих DevTools може утримувати об’єкти під час налагодження й роздувати заміри купи. Для бою й для сесії профілювання це різні історії.
Спільний мотив усіх семи випадків один: хтось тримає «ключ» після того, як мешканець виїхав. Інструменти потрібні, щоб назвати цього тримача на ім’я, а не гадати за симптомами.
Чому це важливо
Витік пам’яті рідко кричить одразу: сторінка «важчає» після навігації в SPA, вкладка з’їдає гігабайти після години роботи, мобільний браузер убиває процес.
Розробник дивиться на CPU і мережу, а корінь — у посиланні, яке забули відпустити при розмонтуванні компонента чи зміні маршруту.
Розуміння досяжності змінює питання з «куди поділася пам’ять?» на три конкретні. Який об’єкт утримується? Який корінь його тримає? На якому кроці життєвого циклу посилання слід було зняти?
Без цього знімки купи перетворюються на список імен класів без дії. З цим — з’являється місце для правки: очищення таймера, зняття слухача, слабкий кеш, відмова від захоплення зайвих даних у замиканні.
Для повного стеку це ще й нагадування: та сама модель досяжності працює в Node.js на V8.
Глобальні кеші, забуті таймери й «вічні» підписки на події сервера дають той самий ефект, що й слухачі на window — лише без вкладки браузера, яку користувач може просто закрити.
На практиці
Спочатку підтвердіть сигнал, потім шукайте ланцюжок — інакше легко «оптимізувати» те, чого немає.
Автор пропонує два кроки в інструментах, а далі дисципліну в коді.
Повторюйте одну й ту саму дію користувача кілька разів між знімками — так простіше відділити шум від справжнього витоку.
- Відкрийте вкладку продуктивності, увімкніть облік пам’яті, запишіть сценарій і кілька разів натисніть примусове збирання сміття (іконка кошика). Якщо базова лінія купи після збирання повзе вгору «сходинками» — це сильний натяк на витік.
- У вкладці пам’яті зробіть знімок купи до й після повторюваної дії користувача; порівняйте об’єкти, виділені між знімками, або фільтр на кшталт об’єктів, утриманих від’єднаними вузлами.
- У списку утримувальних посилань пройдіть шлях від об’єкта до кореня: часто спливає глобальний кеш, замикання слухача чи масив «про всяк випадок».
- У коді заведіть явний розбір життєвого циклу:
clearInterval/cancelAnimationFrame, парнийremoveEventListener, обнулення посилань на DOM після.remove(). - Для метаданих за об’єктами й вузлами віддавайте перевагу
WeakMap, якщо не потрібна ітерація за ключами; для звичайногоMap— ліміт і витіснення. - Під час профілювання не логуйте гігантські структури в консоль — інакше заміри купи брешуть.
- У нестрогому коді ловіть «протікання» в глобал:
const/letі'use strict'закривають цілий клас випадкових витоків ще до профілювальника.
Підсумок
Витік у JavaScript — це майже завжди зайва досяжність, а не «забутий free».
V8 чесно залишає все, до чого можна дотягнутися від кореня; Chrome DevTools допомагає побачити сходинки на графіку й назвати утримувальне посилання.
Дисципліна життєвого циклу — таймери, слухачі, слабкі колекції — закриває більшість побутових випадків із вихідної статті.
Якщо після примусового збирання сміття базова лінія купи досі зростає — спочатку шукайте утримувальне посилання, а не «оптимізацію» алгоритмів.



Коментарі