WeakMap против утечек в event-листенерах: под капотом DOM и кэш
-
Event-листенеры на DOM-нодах часто оставляют утечки памяти. Объект удаляется из DOM, а слушатель держит на него жесткую ссылку через замыкание. WeakMap решает это - слабые ссылки позволяют GC собрать мусор автоматически.
Это критично для SPA с динамическим DOM. Без правильного управления кэш пользовательских данных разрастается, и приложение тормозит. Разберем, как WeakMap чистит за собой, на примерах реального кода.
Классическая утечка: жесткие ссылки в слушателях
Когда добавляешь addEventListener, колбэк захватывает DOM-ноду в замыкании. Нода удаляется из дерева, но слушатель в нейтральной зоне держит ссылку. GC не может ничего собрать - объект висит в памяти зря.
В типичном сценарии SPA рендеришь список пользователей. Каждый элемент с клик-хендлером, который тянет за собой данные профиля. Прокручиваешь список, элементы уходят из вида, но память не освобождается. Через час сессии JS-куча раздувается вдвое.
Проблема усугубляется кэшем. Хранишь userData по ноде в Map - ключ тоже жесткий. Даже если нода ушла, Map не отпустит. Результат: утечка растет линейно с действиями юзера.
- addEventListener без remove: Хендлер держит this и замыкания. Удаляй вручную в cleanup, но забьешь - привет утечка.
- Замыкания с данными: function handler() { use(bigUserObject); } - bigUserObject не уйдет.
- Глобальный кэш: const cache = new Map(); cache.set(node, data); - node жив до конца приложения.
Подход Ссылки на DOM GC работает? Риск утечки Map + жесткие ключи Жесткие Нет Высокий WeakMap Слабые Да Низкий Ручной removeEventListener Жесткие до cleanup Зависит Средний WeakMap под капотом: слабые ключи для нод
WeakMap - это Map, где ключи только объекты, и ссылки на них слабые. GC видит, что на ключ нет других ссылок извне - собирает и ключ, и значение. Идеально для кэша по DOM-нодам.
Под капотом WeakMap не хранит счетчик ссылок явно. Браузерный V8 или SpiderMonkey интегрируют его с марк-энд-свип. Когда нода удалена из DOM и нет других ссылок, WeakMap автоматически ее сбрасывает. Нет нужды в ручной чистке.
Пример с event-листенером. Вместо хранения хендлера в глобале, кэшируй данные по ноде в WeakMap. Хендлер захватывает WeakMap - но это не жесткая ссылка на ноду.
const listeners = new WeakMap(); function attachHandler(node, userData) { listeners.set(node, userData); node.addEventListener('click', function() { const data = listeners.get(node); console.log(data); }); }- Нода удалена: GC видит, что на node нет ссылок кроме WeakMap - чистит node и значение.
- Автоматично: Нет setTimeout для cleanup, нет проверки isMounted.
- Масштабируемо: тысячи нод - память чистится сама.
Кэш пользовательских данных без костылей
Обычный паттерн: кэш профилей по ноде аватара. Map(node => userProfile). Node уходит - профиль висит. WeakMap меняет правила: кэш живет ровно столько, сколько нода.
В реальном проекте SPA с виртуальным скроллом. Рендеришь 1000+ строк таблицы. Каждая с фото, данными юзера. Без WeakMap кэш сожрет гигабайт за сессию. С WeakMap - память стабильна.
Интеграция с событиями. Хендлер клика по аватару тянет tooltip с данными. Данные из WeakMap - если нода жива, данные на месте. Ушла нода - tooltip не сломается, просто undefined.
const userCache = new WeakMap(); document.addEventListener('click', (e) => { if (e.target.dataset.userId) { const node = e.target.closest('.avatar'); const profile = userCache.get(node); if (profile) showTooltip(profile); } });- Производительность: O(1) доступ, как в Map.
- Нюанс: Значения тоже могут быть объектами - GC их тоже проверит.
- Плюс WeakRef: Для значений, если нужно отложенное чтение.
Map WeakMap Пример использования Жесткие ключи Слабые ключи Кэш по DOM-элементам Нужно чистить вручную Авто-GC Event-данные Глобальные ключи Только объекты Пользовательские профили Реальные грабли и как их обойти
Новички лепят WeakMap куда попало, забывая, что ключи - только объекты. Строка в ключ - Error. Или думают, что WeakMap вечный - нет, он чистится при GC.
Частая ошибка: хендлер захватывает переменную извне замыкания. listeners.get(node) внутри, но if (externalVar) {} - externalVar держит все. Делай чистые функции.
В React-подобных фреймворках lifecycle-хуки. useEffect добавляет слушатель - в cleanup removeEventListener. Но с WeakMap можно не чистить: GC сам разберется. Только если хендлер не захвачен родителем.
- Грабли 1:
const strongRef = node;перед WeakMap - убивает слабость. - Грабли 2: Массив нод в глобале - все утечки вернулись.
- Тест: Chrome DevTools > Memory > Heap snapshot. С WeakMap delta = 0.
Когда WeakMap не панацея
WeakMap спасает от 80% утечек в DOM-кэше. Но если данные нужны после удаления ноды - комбинируй с IndexedDB или sessionStorage. Или WeakRef для ленивой загрузки.
Осталось за кадром: полифиллы для старых браузеров, WeakMap в Node.js воркерах. Подумай, как интегрировать с ResizeObserver - там тоже ноды летают. Тестируй в продакшене с реальными данными.
Здравствуйте! Похоже, вас заинтересовала эта беседа, но у вас ещё нет аккаунта.
Надоело каждый раз пролистывать одни и те же посты? Зарегистрировав аккаунт, вы всегда будете возвращаться на ту же страницу, где были раньше, и сможете выбирать, получать ли уведомления о новых ответах (по электронной почте или в виде push-уведомлений). Вы также сможете сохранять закладки и ставить лайки постам, чтобы выразить свою благодарность другим участникам сообщества.
С вашими комментариями этот пост мог бы стать ещё лучше 💗
Зарегистрироваться Войти© 2024 - 2026 ExLends, Inc. Все права защищены.