Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як уникнути витоків пам'яті». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Витік пам'яті у JavaScript це завжди зайве живе посилання: немає посилання, немає витоку.** Тому боротьба з витоками зводиться до того, щоб скорочувати час життя об'єктів і вчасно розривати зв'язки: знімати слухачі подій тим самим колбеком, чистити `setInterval` і `setTimeout`, скасовувати незавершені `fetch` через `AbortController`, не зберігати посилання на видалені з DOM вузли, не замикати великі об'єкти без потреби і обмежувати глобальні кеші. Для кешів за об'єктами беріть `WeakMap`, для кешів за рядками, LRU з жорстким лімітом. У React це cleanup у `useEffect`, у Node.js закриття з'єднань, пулів і стрімів на завершення процесу. **Ключове:** витік шукають вимірюванням, а не здогадками: heap snapshot, пошук «Detached HTML elements» і retainers, плюс тест «прогнати сценарій N разів, пам'ять має стабілізуватися».Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Витоків пам'яті уникають, керуючи посиланнями: немає посилання, немає витоку.** Усе, що лишається досяжним від коренів (глобальні змінні, замикання, DOM, кеші), не буде зібрано збирачем сміття, тому головні практики це скорочувати час життя об'єктів і вчасно розривати зв'язки: обробники, таймери, кеші, посилання на DOM. ## Теорія ### TL;DR - Базовий принцип: **немає посилання, немає витоку**. GC збирає лише недосяжні об'єкти. - Знімайте слухачі подій **тим самим колбеком**, чистіть таймери, скасовуйте `fetch` через `AbortController`. - Не тримайте посилань на **detached** вузли DOM і не замикайте великі об'єкти без потреби. - Кеші: `WeakMap` / `WeakSet` для ключів-об'єктів, **LRU з лімітом** для ключів-рядків. - У React це **cleanup** у `useEffect`, у Node.js це закриття з'єднань, пулів, стрімів і обробка `SIGTERM`. - `FinalizationRegistry` лише для «м'якого» прибирання, бізнес-логіку на ньому не будують. - Шукайте витоки інструментами: heap snapshot, allocation instrumentation, `--trace-gc`, метрики RSS і heapUsed. ### Швидкий приклад ```javascript function mount(button) { const onClick = () => console.log('clicked'); const id = setInterval(tick, 1000); const ac = new AbortController(); button.addEventListener('click', onClick); fetch('https://api.example.com/data', { signal: ac.signal }); // Повертаємо функцію прибирання: усе, що створили, треба зняти return () => { button.removeEventListener('click', onClick); // той самий колбек clearInterval(id); ac.abort(); }; } ``` ### Типові джерела витоків і як їх закрити #### Обробники подій і підписки Проблема: підвислі слухачі тримають і вузол DOM, і всі дані з його замикання. ```javascript const onClick = () => {}; button.addEventListener('click', onClick); // ... button.removeEventListener('click', onClick); // обов'язково тим самим колбеком ``` У React завжди робіть **cleanup** у `useEffect`: ```javascript useEffect(() => { const onScroll = () => {}; window.addEventListener('scroll', onScroll); return () => window.removeEventListener('scroll', onScroll); }, []); ``` У NestJS та в роботі з `EventEmitter` знімайте підписки в `onModuleDestroy()`. #### Таймери та інтервали ```javascript const id = setInterval(tick, 1000); // ... clearInterval(id); ``` У Node.js 20+ для таймерів можна використати **AbortSignal**: ```javascript const ac = new AbortController(); setTimeout(cb, 5000, { signal: ac.signal }); // ... ac.abort(); ``` #### Незавершені fetch і стріми Скасовуйте запити через `AbortController` і закривайте стріми. ```javascript const c = new AbortController(); const res = await fetch(url, { signal: c.signal }); // ... c.abort(); ``` #### Detached вузли DOM Не зберігайте посилань на вузли, видалені з DOM, і обнуляйте кеші з DOM після `remove()`: ```javascript elem.remove(); elemRef = null; // даємо GC шанс ``` #### Великі замикання Не замикайте **великі об'єкти** без потреби, передавайте в колбеки **мінімум** даних, а після використання розривайте посилання: ```javascript let big = getHugeObject(); doWork(() => { /* використовуємо лише невелику частину */ }); big = null; ``` #### Глобальні сінглтони і кеші Усе, що лежить у модулі або в глобальній області, живе «вічно». Обмежуйте розмір, додавайте очищення або слабкі посилання. ### Кеші без витоків Для ключів-об'єктів беріть `WeakMap` або `WeakSet`: вони не заважають збиранню сміття. ```javascript const memo = new WeakMap(); function compute(obj) { if (memo.has(obj)) return memo.get(obj); const val = heavy(obj); memo.set(obj, val); return val; } ``` Для ключів-рядків або чисел слабкі колекції не підходять, тож використовуйте **LRU** з обмеженням: ```javascript class LRU { constructor(limit = 500) { this.limit = limit; this.map = new Map(); } get(k) { const v = this.map.get(k); if (v) { this.map.delete(k); this.map.set(k, v); } return v; } set(k, v) { if (this.map.has(k)) this.map.delete(k); this.map.set(k, v); if (this.map.size > this.limit) { const oldest = this.map.keys().next().value; this.map.delete(oldest); // витісняємо найстаріший запис } } } ``` Окремо: `FinalizationRegistry` годиться лише для **м'якого** прибирання зовнішніх кешів і хендлів. Не покладайтеся на час і порядок виклику, бізнес-логіку на фіналізаторах не будують. ### React і Next.js - **Cleanup** усіх побічних ефектів: `useEffect` має повертати `() => { ... }`. - Не зберігайте величезні об'єкти в `useRef` чи в контексті без потреби. - Уникайте нескінченних ланцюжків мікрозадач із `Promise`: вони затримують і рендер, і збирання сміття. - Відписуйтеся від Observable і WebSocket у cleanup. - Для великих списків використовуйте віртуалізацію, щоб одночасно існувало менше об'єктів. ### Node.js і NestJS - Закривайте з'єднання і пули: - Prisma: `await prisma.$disconnect()` на завершення. - Redis, MongoDB, PostgreSQL: `.quit()`, `.close()`, `.end()` в `onModuleDestroy()`. - Стріми: завжди `.destroy()` або `.end()` при помилках і таймаутах. - `EventEmitter`: `removeListener` або `removeAllListeners` під час вивантаження. - Уникайте безконтрольних `setInterval`, замість них беріть `setTimeout` у циклі з перевірками і можливістю скасування. - Обробляйте сигнали завершення: ```javascript process.on('SIGTERM', async () => { await app.close(); // закриваємо сервер, базу, черги process.exit(0); }); ``` Для асинхронності та черг: ставте **таймаути** і **ліміти на паралелізм** (наприклад, семафори або `p-limit`), у мережі та стрімах тримайте **backpressure**, тобто не читайте швидше, ніж обробляєте, і не тримайте в пам'яті великих масивів промісів, обробляйте чанками. ### Інструменти пошуку витоків і гардрейли #### Браузер - **Memory, Heap snapshot:** шукайте «Detached HTML elements» і retainers. - **Allocation instrumentation:** показує, хто створює і хто утримує об'єкти. - **Performance:** довгі мікрозадачі та часті збирання сміття це запах проблеми. #### Node.js - `--inspect` разом із Chrome DevTools, далі heap snapshots. - `heapdump` або `clinic` для знімків у бойовому середовищі. - Логи збирача: `node --trace-gc app.js`, для діагностики, не для продакшену. - Метрики: моніторте RSS і `heapUsed`, ставте алерти на тренд зростання. #### Гардрейли в коді та CI - Ліміти на розміри кешів і черг. - E2E-тест: «прогнали сценарій N разів, пам'ять стабілізувалася». - Правила лінтера: заборона анонімних слухачів там, де потрібне зняття, і заборона `setInterval` без явного `clearInterval`. ### Типові помилки - **Зняття слухача іншою функцією.** `removeEventListener('click', () => {})` не знімає нічого: потрібне те саме посилання на колбек, що і при додаванні. - **Забутий `clearInterval` під час розмонтування.** Інтервал переживає компонент і тримає все його замикання. - **Збереження посилання на вузол після `remove()`.** Вузол стає detached, але живим, разом із усім піддеревом. - **Необмежений кеш у модульній області.** Глобальний `Map` без витіснення росте, доки живе процес. - **Спроба використати `WeakMap` з ключами-рядками.** Слабкі колекції приймають лише об'єкти, для примітивних ключів потрібен LRU. - **Надія на `FinalizationRegistry` замість явного закриття ресурсів.** Колбек може не виконатися ніколи. - **Виклик `global.gc()` або сподівання, що `= null` негайно звільнить пам'ять.** Час збирання визначає рантайм. - **Пошук витоку «на око» замість знімків купи.** Без порівняння двох snapshot і аналізу retainers ви лише вгадуєте.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.