Рост heap в Node.js означает увеличение объема выделенной памяти процессором V8. Это не всегда критическая ошибка, но требует внимания, так как бесконечный рост ведет к исчерпанию лимитов ОС и падению приложения. Диагностика начинается с наблюдения за динамикой использования памяти во времени, а не с анализа статического кода.

1. Подготовка к диагностике

Перед началом анализа убедитесь, что приложение работает в стабильном состоянии. Запустите его с флагом `—max-old-space-size=4096` (или другим разумным лимитом), чтобы избежать мгновенного краша при пиковых нагрузках.

Используйте команду `node —inspect app.js` для запуска приложения в режиме отладки. Это позволит подключиться к профилировщику позже.

Подготовьте инструмент мониторинга. Для локальной разработки подойдет Chrome DevTools (Remote Debugging), для продакшена — Prometheus с exporter node_exporter или специализированные инструменты типа Clinic.js.

2. Анализ динамики использования памяти

Используйте встроенный метод `process.memoryUsage()` для получения текущих значений. Он возвращает объект с полями `heapUsed`, `heapTotal`, `rss`.

Пример кода для отслеживания в консоли:

«`javascript
setInterval(() => {
console.log(process.memoryUsage());
}, 5000);
«`

Запустите приложение под нагрузкой (например, имитируя входящие запросы). Наблюдайте за графиком `heapUsed`. Если значение растет линейно и не возвращается к базовому уровню после завершения обработки задачи — это признак утечки.

Важно: различайте рост heap из-за реальной работы (кэширование, обработка больших данных) и патологический рост. В первом случае память освобождается по мере необходимости, во втором — старые объекты остаются в памяти.

3. Поиск утечек с помощью профилировщика V8

Подключитесь к процессу через Chrome DevTools (вкладка `Profiles` -> `Heap Snapshot`). Это самый эффективный способ найти объекты, которые накапливаются.

Создайте первый снимок heap (`Take Heap Snapshot`) в момент низкой нагрузки.

Запустите приложение под нагрузкой до появления роста памяти.

Создайте второй снимок heap.

В Chrome DevTools перейдите во вкладку `Compare`. Выберите два снимка и нажмите `Compare snapshots`.

Проанализируйте список объектов. Ищите те, у которых разница в количестве экземпляров (`Count`) между снимками значительна, а размер (`Size`) тоже растет.

Нажмите на подозрительный объект, чтобы увидеть его свойства. Часто утечки связаны с замыканиями внутри циклов или обработчиков событий, которые ссылаются на большие данные.

4. Типичные причины роста heap и способы решения

**Замыкания в циклах**: Если вы создаете функцию внутри цикла и сохраняете её в массив или глобальную переменную, она захватывает переменную из области видимости цикла. Даже после завершения цикла объект остается в памяти.

Решение: Используйте `let` вместо `var` для создания замыканий внутри циклов, чтобы каждая итерация имела свою область видимости. Избегайте сохранения функций в глобальные массивы без очистки.

**Глобальные переменные**: Объекты, добавленные в глобальный контекст (например, `global.someObject`), никогда не удаляются автоматически. Если вы постоянно добавляете данные в такие объекты, heap будет расти бесконечно.

Решение: Избегайте использования глобальных переменных для хранения временных данных. Используйте модульную архитектуру или локальные переменные внутри обработчиков запросов.

**Неочищенные события**: Если вы подписываетесь на событие (например, `process.on('data', …)`) и не отписываетесь после завершения работы, обработчик может накапливать данные.

Решение: Всегда используйте `removeListener` или `off`, когда обработка завершена. Особое внимание уделите событиям `uncaughtException` и `SIGTERM` для очистки ресурсов перед завершением процесса.

**Буферы и строки**: В Node.js строки и буферы занимают память. Если вы создаете много строк из больших байтовых данных и не конвертируете их обратно, память может расти.

Решение: Используйте `Buffer.allocUnsafe` для временных буферов и освобождайте их явно через `buffer.fill(0)` или переназначением переменной (`buffer = null`). Избегайте создания новых строк из больших байтовых данных без необходимости.

5. Как оценить результат диагностики

После внесения изменений снова запустите приложение под нагрузкой и отслеживайте `heapUsed`.

Если память растет до определенного уровня, а затем стабилизируется или немного снижается — утечка устранена.

Если heap продолжает расти линейно без стабилизации — проблема не решена или есть несколько источников утечки.

Используйте метрику «время жизни объекта»: если объект создается в момент времени T1 и удаляется в T2, разница (T2 — T1) должна быть разумной. Если объекты живут дольше ожидаемого срока — это утечка.

6. Ограничения и предостережения

Не полагайтесь только на `process.memoryUsage()`. Этот метод показывает текущее использование, но не историю освобождения памяти.

В Node.js нет явного освобождения памяти (garbage collection происходит автоматически). Если вы удаляете ссылку на объект, он будет удален только после следующего цикла сборки мусора (GC).

Иногда рост heap вызван кэшированием данных (например, в Redis или встроенном кэше приложения). Это не утечка, если данные действительно нужны. Оцените, нужно ли хранить эти данные в памяти или лучше использовать внешнее хранилище.

Не игнорируйте ошибки `FATAL ERROR: Ineffective heap-compaction min-decr-size 1024`. Это означает, что сборщик мусора не может освободить достаточно памяти. В этом случае нужно искать утечки и увеличивать лимит heap или оптимизировать код.

Вопросы и ответы

Можно ли увеличить `—max-old-space-size` как решение проблемы?

Нет, это временная мера. Увеличение лимита только откладывает момент падения приложения. Если есть утечка памяти, приложение все равно упадет, просто позже. Нужно найти и устранить причину роста heap.

Как отличить утечку памяти от нормального роста из-за кэширования?

Если приложение обрабатывает уникальные данные (например, каждый запрос содержит новый документ), то рост heap может быть ожидаемым. Если же приложение обрабатывает повторяющиеся запросы и память растет без освобождения старых данных — это утечка. Используйте профилировщик для сравнения снимков heap.

Почему утечки памяти в Node.js сложнее найти, чем в других языках?

В Node.js сборка мусора автоматическая и не требует явного вызова. Разработчики часто забывают удалить ссылки на объекты, полагаясь на GC. Кроме того, V8 использует сложную систему управления памятью (разделение heap на old и young spaces), что затрудняет анализ без профилировщика.

По теме

// Пример отслеживания памяти
setInterval(() => {
  const mem = process.memoryUsage();
  console.log(`Heap used: ${mem.heapUsed} bytes`);
}, 5000);

// Пример утечки через замыкание в цикле (неправильно)
const badArray = [];
for (let i = 0; i < 1000; i++) {
  badArray.push(() => {
    return `data-${i}`;
  });
}

// Правильный вариант с let
const goodArray = [];
for (let i = 0; i < 1000; i++) {
  let currentI = i;
  goodArray.push(() => {
    return `data-${currentI}`;
  });
}