Каждый раз, когда слышу "у нас утечка памяти", первая мысль - кто-то забыл удалить слушатель событий или не очистил setInterval. В production все сложнее: утечки копятся неделями, проявляются при определенной нагрузке, и локально их не воспроизвести.
Вот три рабочих инструмента.
Heap snapshots: как читать между строк
Снимаешь снапшот в Chrome DevTools (Memory -> Take heap snapshot). Сравниваешь два: до и после выполнения подозрительного сценария.
Ищешь объекты, которые:
- Не должны быть в памяти, но есть (старые DOM-ноды с замыканиями)
- Имеют неожиданно много экземпляров (10 000 объектов класса
Widget)Если видишь
Closure с огромным retained size - это замыкание, которое держит ссылку на внешние данные. В production такая утечка часто возникает в SSR, когда обработчик запроса захватывает ссылку на глобальный кэш.Memory tab: интерактивный детектив
Переходишь на вкладку Memory, выбираешь "Allocation instrumentation on timeline". Записываешь профиль при активной работе пользователя.
Красные пики в столбчатой диаграмме - места, где память выделяется, но не освобождается. Клик по пику покажет стек вызовов в момент аллокации.
Паттерн, который ловил так: React-компонент, при каждом ререндере создавал новый объект-конфигурацию и передавал в children - GC не успевал чистить, через час работы страница весила 200+ МБ.
Автоматизированные паттерны детекции
Ручной анализ хорошо, но в production нужно автоматическое обнаружение. Два подхода.
Performance Observer с проверкой usedJSHeapSize:
new PerformanceObserver((list) => {
const entries = list.getEntries();
if (performance.memory?.usedJSHeapSize > 200_000_000) {
console.warn('Memory warning', performance.memory);
}
}).observe({ type: 'resource', buffered: true });Только в Chrome с флагом
enable-experimental-web-platform-features. Типичная ошибка - использовать это без fallback в Safari или Firefox.Кастомный мониторинг:
setInterval(() => {
const { usedJSHeapSize, totalJSHeapSize } = performance.memory;
const usagePercent = (usedJSHeapSize / totalJSHeapSize) * 100;
if (usagePercent > 80) {
sendMetric('memory_leak_risk', usedJSHeapSize);
}
}, 60000);Не используй для продакшена бездумно -
performance.memory есть не везде. Оборачивай в try/catch и делай fallback на navigator.userAgent.Утечки в production - это не баги, а архитектурные проблемы. Если после каждого перехода по роуту память растет на 5-10% и не падает - ищи подписки в эффектах, которые не отписываются при размонтировании. Или глобальные кэши, которые никогда не чистятся.
Вывод: Надежная детекция утечек требует сочетания автоматических инструментов и ручного анализа heap snapshots, а не надежды на GC или банальные логи.
