JavaScript звільняє пам'ять автоматично: збирач сміття видаляє об'єкти, до яких неможливо дістатися від «коренів» (глобальних об'єктів, стеку викликів, активних обробників). Витік - це об'єкт, який уже не потрібен, але на який досі хтось посилається.
Замикання тримає змінні своєї області видимості, поки живе сама функція:
function attachHandler(element) {
const hugeReport = buildReport(); // 50 МБ даних
element.addEventListener('click', () => {
console.log(hugeReport.title); // замикання тримає hugeReport
});
}
Поки обробник зареєстрований, hugeReport у пам'яті. Якщо обробнику потрібен лише заголовок - краще захопити лише його: const title = hugeReport.title.
Типові джерела витоків у фронтенді:
- обробники на глобальних об'єктах (
window,document), додані компонентом і не зняті при його знищенні:
// при кожному монтуванні компонента - новий обробник, старі живуть вічно
window.addEventListener('resize', this.onResize);
- таймери:
setInterval, що посилається на дані компонента, працює й після того, як компонент зник зі сторінки; - від'єднані вузли DOM: елемент видалено зі сторінки, але посилання на нього лишилося в масиві, кеші чи замиканні - разом з ним живе все його піддерево;
- кеші без обмеження:
Map, куди дані лише додають; - спостерігачі й підписки:
IntersectionObserver, WebSocket-обробники, підписки на сховище стану.
Як прибирати:
const controller = new AbortController();
window.addEventListener('resize', onResize, { signal: controller.signal });
document.addEventListener('keydown', onKey, { signal: controller.signal });
// при знищенні компонента - одним викликом знімаються всі обробники
controller.abort();
У фреймворках для цього є хуки знищення: onUnmounted у Vue, функція очищення в useEffect React, destroy() в Alpine-компонентах.
Для кешів - WeakMap (запис зникає разом з об'єктом-ключем) чи обмеження розміру з витісненням.
Як знайти витік: у DevTools вкладка Memory - зробити знімок купи, повторити дію кілька разів (відкрити й закрити модальне вікно), зробити ще знімок і порівняти. Об'єкти, кількість яких росте з кожним повтором, і поле Detached для від'єднаних вузлів DOM вказують на винуватця. Панель Retainers показує ланцюжок посилань, що тримає об'єкт.
Особливо важливо для SPA і wire:navigate: сторінка не перезавантажується, тож витоки накопичуються годинами роботи.