Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Витік пам'яті в JS». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Витік пам'яті (memory leak) це ситуація, коли програма утримує посилання на об'єкти, які вже не потрібні, тому збирач сміття не може звільнити пам'ять.** Збирач сміття видаляє лише недосяжні об'єкти, тобто ті, до яких не веде жодне посилання; якщо десь лишилося «забуте» посилання, об'єкт вважається живим і залишається в heap. Класичні джерела: неявні глобальні змінні, неочищені `setInterval`, не знятий `addEventListener`, замикання над великими структурами, кеші без ліміту та посилання на видалені DOM-вузли. ```javascript function start() { const data = new Array(1e6).fill(0); setInterval(() => console.log(data.length), 1000); } start(); // data ніколи не звільниться: її тримає замикання інтервалу ``` **Ключове:** витік, це не «баг збирача сміття», а зайве посилання, яке ви забули прибрати.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Витік пам'яті це ситуація, коли програма утримує посилання на об'єкти, які вже не потрібні, тому збирач сміття (Garbage Collector) не може звільнити їх з пам'яті.** Іншими словами, об'єкт більше не використовується, але через «забуті» посилання залишається в пам'яті і поступово з'їдає heap. ## Теорія ### TL;DR - Витік, це утримання непотрібних об'єктів у пам'яті через посилання, які ніхто не прибрав. - Збирач сміття в JS видаляє лише недосяжні об'єкти: якщо є хоч одне посилання, об'єкт живий. - Основні причини: неявні глобальні змінні, неочищені таймери, не зняті слухачі подій, замикання над великими даними, кеші без ліміту, посилання на видалені DOM-вузли. - Ознаки: heap росте від знімка до знімка, з'являються detached DOM nodes, інтерфейс поступово гальмує. - Діагностика: Chrome DevTools, вкладка Memory і heap snapshot; у Node.js, `--inspect`, `heapdump`, `process.memoryUsage()`. - Профілактика: cleanup таймерів і слухачів, обмежені кеші, `WeakMap` і `WeakSet` зі слабкими посиланнями. ### Швидкий приклад ```javascript const btn = document.getElementById('btn'); function handleClick() { console.log('clicked'); } btn.addEventListener('click', handleClick); btn.remove(); // елемент прибрано з DOM, але слухач лишився // handleClick і сам btn залишаються в пам'яті: це detached DOM node ``` Правильний варіант: перед видаленням вузла зняти слухача через `btn.removeEventListener('click', handleClick)` і обнулити зовнішні посилання на елемент. ### Як працює збирач сміття і звідки береться витік JavaScript має автоматичне керування пам'яттю. Збирач сміття періодично шукає **досяжні** об'єкти, тобто ті, до яких можна дійти по ланцюжку посилань від кореневих об'єктів (глобальний об'єкт, стек викликів, активні замикання). - Усе, до чого немає посилань (недосяжне), видаляється. - Усе, на що є хоча б одне посилання, лишається в пам'яті. Саме звідси й береться проблема: об'єкт більше не потрібен логічно, але десь лишилося посилання на нього, тому збирач сміття вважає його «живим» і не звільняє. Важливо: для рушія немає різниці між потрібним і забутим посиланням, тож витік, це завжди помилка в коді, а не в збирачі сміття. ### Типові причини витоків | Причина | Приклад | Що відбувається | | --- | --- | --- | | **Глобальні змінні** | `window.obj = { big: 'data' }` | живуть до закриття вкладки | | **Неочищені таймери та інтервали** | `setInterval(() => {...}, 1000)` без `clearInterval` | колбек тримає замикання і не звільняється | | **Слухачі подій** | `element.addEventListener('click', handler)` без `removeEventListener` | елемент видалений, а обробник лишився в пам'яті | | **Замикання над великими даними** | функції, що захоплюють контекст з великими об'єктами | збирач не може звільнити контекст | | **Кеші та структури даних** | `Map`, що росте без очищення старих ключів | дані не видаляються автоматично | | **DOM-посилання після видалення вузла** | `const el = document.getElementById('app'); el.remove();` | вузла вже немає в DOM, але змінна `el` тримає його | Неявна глобальна змінна: ```javascript function createLeak() { leak = []; // без let / const змінна потрапляє у window for (let i = 0; i < 1000000; i++) leak.push(i); } createLeak(); // leak живе до закриття вкладки ``` Таймер, який ніхто не зупиняє: ```javascript function start() { const data = new Array(1e6).fill('x'); setInterval(() => console.log(data.length), 1000); } start(); // data ніколи не очищається, її тримає замикання колбека ``` Кеш без ліміту: ```javascript const cache = new Map(); function heavyCalc(key) { if (cache.has(key)) return cache.get(key); const result = Array(1e5).fill(key); cache.set(key, result); // жоден запис ніколи не видаляється return result; } ``` ### Як виявити витік пам'яті **У браузері (Chrome DevTools):** 1. Відкрийте вкладку **Memory** і оберіть **Heap snapshot**. 2. Зробіть знімок пам'яті (Take snapshot). 3. Повторіть дію, яку підозрюєте (відкрити і закрити модалку, перейти між сторінками), і зробіть другий знімок через 10 або 30 секунд. 4. Порівняйте знімки в режимі Comparison: якщо кількість об'єктів або записів `Detached DOM nodes` стабільно зростає, це витік. Також корисна вкладка **Performance** з увімкненим прапорцем Memory: там видно, чи heap size повертається до початкового рівня після збирання сміття, чи лінія стабільно повзе вгору. **У Node.js:** - `node --inspect` і Chrome DevTools для heap snapshot. - Інструменти `clinic.js`, `node --inspect-brk`, пакет `heapdump`. - Періодичний моніторинг через `process.memoryUsage()` і метрики `heapUsed` та `rss`. **Ознаки витоку без профайлера:** - Застосунок поступово сповільнюється під час довгої сесії. - Пам'ять росте і не повертається назад після навігації. - Затримки в інтерфейсі, особливо під час скролу та рендерингу. - Часті й довгі паузи на збирання сміття. - Вкладка врешті падає з повідомленням про брак пам'яті. ### Як запобігати витокам | Проблема | Рішення | | --- | --- | | Глобальні змінні | Завжди оголошувати через `let` і `const`, вмикати strict mode | | Таймери | Викликати `clearTimeout` і `clearInterval` при зупинці | | Слухачі подій | Знімати через `removeEventListener` при розмонтуванні | | Замикання | Не тримати великі об'єкти в області видимості довгоживучих функцій | | Кеші | Обмежувати розмір і чистити старі записи, або брати `WeakMap` / `WeakSet` | | DOM | Разом з видаленням вузла прибирати всі змінні, що на нього посилаються | | SPA-застосунки | Стежити за cleanup у `useEffect`, `onUnmounted` та подібних хуках | Окремий інструмент, це `WeakMap` і `WeakSet`. Вони тримають **слабкі посилання**, тобто не заважають збиранню сміття: ```javascript const cache = new WeakMap(); function getData(obj) { if (cache.has(obj)) return cache.get(obj); const data = heavyCalculation(obj); cache.set(obj, data); return data; } ``` Якщо об'єкт `obj` більше ніде не використовується, збирач сміття видалить його автоматично, навіть коли він є ключем у `WeakMap`. Ціна: такі колекції не ітеруються і не мають `size`, а ключами можуть бути лише об'єкти. Коротке резюме: | Що | Опис | | --- | --- | | **Memory leak** | Утримання непотрібних об'єктів у пам'яті | | **Причина** | Посилання, які не звільняються | | **Наслідок** | Зростання пам'яті, лаги, падіння вкладки | | **Діагностика** | Chrome DevTools, вкладка Memory і heap snapshot | | **Рішення** | Чистити таймери, слухачів, кеші та DOM-посилання | | **Профілактика** | `WeakMap`, `WeakSet`, cleanup-хуки | ### Типові помилки 1. **Вважати, що збирач сміття рахує посилання.** Сучасні рушії використовують досяжність (mark and sweep), тому циклічні посилання самі по собі не є витоком: дві структури, що посилаються одна на одну, але недосяжні ззовні, буде зібрано. 2. **Прибирати слухача анонімною функцією.** `removeEventListener('click', () => {...})` не спрацює: потрібне те саме посилання на функцію, що передавали в `addEventListener`. 3. **Забувати cleanup у компонентах.** `useEffect` без функції очищення лишає підписки, таймери й `AbortController` живими після розмонтування компонента. 4. **Плутати високе споживання пам'яті з витоком.** Застосунок може просто тримати великий легітимний стан; витік, це коли пам'ять росте і **не повертається** після збирання сміття. 5. **Вважати `WeakMap` універсальним рішенням.** Слабким є лише ключ; якщо значення посилається назад на ключ або на щось велике й досяжне, пам'ять не звільниться. 6. **Робити висновки за одним знімком heap.** Витік видно лише в динаміці: потрібні щонайменше два знімки після однакового сценарію і примусового збирання сміття. 7. **Тримати в замиканні весь великий об'єкт заради одного поля.** Достатньо скопіювати потрібне значення (`const { id } = user`), і решта структури зможе бути зібраною.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.