Управление памятью в Node.js: куча, утечки, профилирование
Содержание
О проверке кода. Примеры не запускались в этой сессии (по договорённости — фокус на контенте, а не прогонах). Говорим прямо, а не ставим бейдж «проверено».
Node.js управляет памятью автоматически через сборщик мусора движка V8. Но «автоматически» не значит «беспроблемно»: на сервере, работающем сутками, утечки и упор в лимит памяти — реальная угроза. Разберём, как это контролировать.
Измерение: process.memoryUsage
const usage = process.memoryUsage();
usage.rss; // Resident Set Size — вся память процесса
usage.heapTotal; // выделено под кучу V8
usage.heapUsed; // реально занято объектами в куче
usage.external; // память вне кучи (буферы, C++ объекты)
// в мегабайтах, читаемо
console.log(`Куча: ${(usage.heapUsed / 1024 / 1024).toFixed(1)} МБ`);
process.memoryUsage() — основа мониторинга памяти. Ключевые поля:
rss— вся память процесса в ОЗУ (куча + стек + код);heapUsed— сколько реально занято объектами JavaScript. Именно за его ростом следят при поиске утечек;external— память буферов и нативных объектов вне кучи V8.
Периодический вывод heapUsed в лог — простейший способ заметить, что память ползёт вверх и не возвращается.
Как устроена куча V8
Куча V8 (heap)
Объекты JavaScript
- Здесь живут все объекты, строки, массивы
- Управляется сборщиком мусора
- Ограничена лимитом (~2–4 ГБ)
heapUsed— что занято
Вне кучи
Буферы и служебное
- Buffer и нативные объекты (
external) - Стек вызовов
- Код и метаданные
- Не ограничено лимитом кучи
Объекты JavaScript живут в куче V8, которая ограничена по размеру. Сборщик мусора освобождает недостижимые объекты, но сама куча не может расти бесконечно — есть лимит. Буферы и бинарные данные хранятся вне кучи (external), поэтому обработка больших файлов через буферы не упирается в лимит кучи так быстро.
Лимит памяти и его изменение
# по умолчанию куча ограничена (порядок 2–4 ГБ в зависимости от версии)
node app.js
# поднять лимит до 4 ГБ
node --max-old-space-size=4096 app.js
Куча V8 имеет лимит, и упор в него роняет процесс с JavaScript heap out of memory. Флаг --max-old-space-size (в мегабайтах) поднимает потолок — это спасает при легитимной большой нагрузке (обработка крупных данных). Но если память растёт бесконечно, увеличение лимита лишь оттягивает падение: настоящая причина — утечка, и лечить надо её, а не потолок.
Утечки памяти
Утечка — когда объекты достижимы, но уже не нужны, и сборщик не может их удалить. Причины те же, что в браузере, но на сервере опаснее — процесс живёт долго:
// ❌ глобальный кэш растёт без предела — классика серверной утечки
const cache = new Map();
app.get('/data/:id', (req, res) => {
cache.set(req.params.id, expensiveComputation()); // никогда не чистится
res.json(cache.get(req.params.id));
});
Частые источники на сервере:
- растущие коллекции — кэш, массив истории,
Mapбез ограничения и очистки; - незакрытые таймеры —
setIntervalбезclearInterval; - накопление слушателей —
emitter.onв обработчике запроса без снятия (предупреждение о утечке слушателей); - замыкания, держащие большие объекты.
На долгоживущем сервере даже маленькая утечка на каждый запрос за сутки съедает всю память.
Поиск утечки: heap snapshot
// быстрая диагностика: логировать heapUsed под нагрузкой
setInterval(() => {
const mb = (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1);
console.log(`heapUsed: ${mb} МБ`);
}, 10000);
Если heapUsed растёт и не возвращается после сборок мусора — есть утечка. Дальше — точный поиск через heap snapshot:
- запустить с
--inspect, открыть DevTools → Memory; - снять snapshot, дать нагрузку, снять второй;
- сравнить (Comparison) — что за объекты выросли в числе;
Растущий тип объектов укажет и на причину, и на место, где копятся ссылки. Это стандартная процедура диагностики утечек в production.
Главный приём экономии: потоки
// ❌ грузит весь файл в память — на 4 ГБ файле упадёт
const data = fs.readFileSync('huge.csv');
// ✅ поток — в памяти лишь текущий кусок
fs.createReadStream('huge.csv')
.pipe(parser)
.pipe(writer);
Главный способ не забивать память — не загружать данные целиком. readFile читает весь файл в кучу; потоки обрабатывают его по кускам, держа в памяти лишь текущий фрагмент. Файл на 4 ГБ через readFile уронит процесс, а через поток обработается на скромной памяти. Это же касается больших ответов БД и сетевых данных — думайте потоками, а не «загрузить всё, потом обработать».
Практический чек-лист
- логируйте
heapUsedв production — заметите рост заранее; - ограничивайте размер кэшей, чистите старое, используйте
WeakMapгде уместно; - закрывайте таймеры и снимайте слушатели;
- большие данные — только потоками;
- утечку лечите причиной, а не поднятием лимита;
- при легитимной большой нагрузке —
--max-old-space-size.
Смежные темы: сборка мусора — как освобождается память; потоки — обработка больших данных; Buffer — память вне кучи; отладка — профилирование. Полный список — в уроках Node.js.