Какие проблемы с утечкой памяти могут возникнуть при большом количестве подписок и как их избежать?
Что проверяют
Понимаете ли вы механизм утечки: подписка хранит колбэк, колбэк через замыкание держит компонент и его DOM, а источник живёт дольше компонента. Следом почти всегда спрашивают, как вы найдёте такую утечку в DevTools.
Как отвечать
Начните с источников долгоживущих ссылок: window и document, глобальный стор, WebSocket, таймеры, наблюдатели, RxJS-потоки. Компонент размонтирован, а подписка осталась, и сборщик мусора не может освободить ни замыкание, ни отсоединённые DOM-узлы. При сотнях подписок память растёт, а обработчики срабатывают для мёртвых экранов и дублируют запросы.
Правила по стеку:
- React: подписка в
useEffect, отписка в функции очистки.StrictModeв разработке монтирует эффект дважды и вскрывает забытую очистку. Для внешних сторов естьuseSyncExternalStore; - AbortController:
addEventListener(type, fn, { signal })снимает все слушатели однимabort(), тот же сигнал отменяетfetch; - Angular и RxJS:
asyncpipe отписывается сам, в коде компонентаtakeUntilDestroyed(); - Vue:
onBeforeUnmount, аwatch, созданный вsetup, останавливается автоматически; - таймеры и наблюдатели:
clearInterval,observer.disconnect(),socket.close(); - много однотипных подписок: делегирование событий на общего родителя вместо слушателя на каждой строке списка, один
ResizeObserverна все элементы.
Диагностика: вкладка Memory, снимок кучи до и после нескольких открытий и закрытий экрана, сравнение снимков и фильтр Detached для отсоединённых узлов.
Где спотыкаются
Передают в removeEventListener новую стрелочную функцию. Это другая ссылка, слушатель остаётся. То же, если при добавлении был capture: true, а при снятии нет.
Считают, что WeakMap или Redux решают проблему сами. WeakMap хранит метаданные объектов, но подписку не отменяет, а store.subscribe возвращает функцию отписки, которую надо вызвать.
Подписываются в обработчике клика, вне эффекта. Очистка тогда не срабатывает, и каждое нажатие добавляет ещё одну подписку.