Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Таймери та слухачі подій». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Тому що активний таймер або підписаний слухач це живе посилання: поки він існує, збирач сміття вважає досяжним увесь контекст його колбека і не може звільнити жодного байта.** `setTimeout`, `setInterval` і `addEventListener` створюють асинхронні задачі, які живуть окремо від поточного стека викликів, а їхні колбеки через замикання тримають усе, до чого звертаються: великі масиви, DOM-вузли, екземпляри компонентів. Видалити елемент зі сторінки недостатньо, бо слухач лишається зареєстрованим, і вузол перетворюється на detached DOM node. Додатково старі слухачі спрацьовують разом із новими, що дає дублювання запитів і зростання CPU. ```javascript useEffect(() => { const id = setInterval(tick, 1000); return () => clearInterval(id); // without this the closure lives forever }, []); ``` **Ключове:** `clearInterval`, `clearTimeout`, `removeEventListener` і `unsubscribe` це не гігієна, а єдиний спосіб дозволити GC звільнити пам'ять.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Таймери і слухачі подій треба очищати тому, що кожен із них це живе посилання на контекст свого колбека, а GC звільняє лише недосяжні об'єкти.** `setTimeout`, `setInterval` і `addEventListener` створюють асинхронні задачі, які живуть окремо від основного стека виконання JS, і поки така задача активна, все, до чого звертається її callback, вважається досяжним. ## Теорія ### TL;DR - Таймер або слухач тримає замикання, а замикання тримає всі змінні, до яких звертається колбек. - Поки посилання живе, GC вважає об'єкти досяжними і не звільняє пам'ять. - Видалення DOM-елемента не знімає слухача: вузол стає detached DOM node і лишається в heap. - У SPA це накопичується, бо сторінка не перезавантажується до закриття вкладки. - Крім пам'яті, страждає CPU: сотні активних інтервалів і дублювання спрацювань старих слухачів. - Лікування просте: `clearInterval` / `clearTimeout`, `removeEventListener`, `unsubscribe`, `return () => cleanup()` в `useEffect`. ### Швидкий приклад ```javascript function startTimer() { const bigData = new Array(1e6).fill('x'); setInterval(() => console.log(bigData.length), 1000); } startTimer(); ``` Тут `bigData` ніколи не звільняється: - інтервал живе вічно; - замикання тримає посилання на `bigData`; - GC вважає `bigData` «живим», і пам'ять росте. Треба очищати: ```javascript const id = setInterval(tick, 1000); clearInterval(id); ``` ### Що саме утримує пам'ять Зі слухачами подій історія та сама: ```javascript const button = document.getElementById('btn'); button.addEventListener('click', () => console.log('clicked')); button.remove(); // the element is gone, the listener is not ``` Тепер елемент `button` видалено з DOM, але callback досі зберігається в пам'яті, бо на нього лишилося посилання з системи подій. GC не може прибрати вузол, бо на нього «дивиться» зареєстрований обробник. Саме такі вузли видно в heap snapshot як **Detached DOM nodes**. Рішення: ```javascript button.removeEventListener('click', handler); ``` Зверніть увагу: зняти анонімну стрілкову функцію неможливо, бо `removeEventListener` порівнює посилання. Обробник треба зберігати в змінній. | Об'єкт | Посилання | Чому не очищається | | --- | --- | --- | | Таймер | Колбек (closure) | Тримає посилання на всі змінні замикання | | Слухач | Callback плюс DOM-елемент | Живуть, поки не викликати `removeEventListener` | | WebSocket, RxJS, `setInterval` | Активна підписка | Тримає весь контекст підписки | | React `useEffect` | Без cleanup | Ефект не скасовується, посилання зберігаються | GC вважає все це досяжним саме тому, що має активні посилання на колбеки. ### Чому це особливо критично в SPA > У SPA сторінка не перезавантажується. Усе, що не очищається, живе, поки користувач не закриє вкладку. Наприклад: - ви переходите між сторінками; - компоненти монтуються і розмонтовуються; - якщо таймери і слухачі не знімаються, кожен «старий» компонент продовжує існувати в пам'яті, і сміття накопичується. Через 30 хвилин роботи React-застосунку можна побачити: - сотні активних інтервалів; - десятки «зомбі»-слухачів; - heap memory, що росте і не повертається до базового рівня. ### Як правильно очищати Таймери: ```javascript useEffect(() => { const id = setInterval(() => { /* ... */ }, 1000); return () => clearInterval(id); }, []); ``` Слухачі подій: ```javascript useEffect(() => { const handler = () => console.log('scroll'); window.addEventListener('scroll', handler); return () => window.removeEventListener('scroll', handler); }, []); ``` Підписки (RxJS, WebSocket, власні події): ```javascript useEffect(() => { const sub = stream$.subscribe(handleValue); return () => sub.unsubscribe(); }, []); ``` Для слухачів, які потрібно зняти пачкою, зручний `AbortController`: усі підписки з одним сигналом знімаються одним викликом `abort()`. ### Що станеться, якщо не чистити | Наслідок | Що відбувається | | --- | --- | | Зростання пам'яті | Замикання колбеків утримують великі об'єкти | | Лаги UI | Event loop забитий активними колбеками | | Дубльовані виклики | Старі слухачі спрацьовують разом із новими | | Зростання CPU | Забагато активних інтервалів працюють паралельно | | «Out of memory» | Браузер або вкладка падає | | «Зомбі-компоненти» | Старі React-компоненти живуть далі і виконують ефекти | Інструменти, якими це видно: - **Chrome DevTools, Memory, Heap snapshot**: шукаємо Detached DOM nodes і об'єкти `Timeout`, `EventListener`. - **Performance, Record**: дивимось на Long tasks і графік пам'яті. - **React DevTools Profiler**: перевіряємо, що компоненти справді розмонтовуються. - **Sentry Performance**: зростання heap, витоки, деградація FPS на проді. Підсумок: | Що треба пам'ятати | Чому | | --- | --- | | `setTimeout` / `setInterval` треба очищати | Інакше callback лишається живим | | `addEventListener` треба знімати | Інакше DOM-вузол і функція не звільняються | | У React завжди `return () => cleanup()` в `useEffect` | Запобігає «зомбі»-ефектам | | У SPA немає «перезапуску» пам'яті | Усе копиться, поки не очистиш вручну | | Витоки пам'яті це лаги плюс падіння | GC не може прибрати «живі» посилання | > Таймер або слухач це як відкрита вкладка в пам'яті. Якщо не закрити, таких вкладок стають сотні, і браузер «помирає». ### Типові помилки - **Підписуватися анонімною функцією.** `addEventListener('click', () => ...)` неможливо зняти: `removeEventListener` порівнює посилання на функцію, тому обробник треба тримати у змінній. - **Вважати, що `element.remove()` знімає слухачів.** Не знімає; вузол лишається в пам'яті як detached DOM node, поки живе обробник. - **Не зберігати id таймера.** `setInterval(fn, 1000)` без збереженого id уже не зупинити з коду. - **Знімати слухача з іншого елемента або з іншим типом події.** `removeEventListener` спрацює лише при повному збігу цілі, типу події і опцій (наприклад `capture`). - **Забувати cleanup у `useEffect` з порожнім масивом залежностей.** Такий ефект виглядає як «одноразовий», але виконується на кожному монтуванні компонента. - **Ігнорувати підписки, які не схожі на слухачів.** `IntersectionObserver`, `ResizeObserver`, `MutationObserver` і WebSocket теж треба зупиняти явно: `disconnect()`, `close()`, `unsubscribe()`.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.