Кэш карт исходного кода Node представляет собой скрытую утечку памяти в режиме разработки.
Узнайте, почему включение параметров --enable-source-maps или NODE_V8_COVERAGE может привести к неограниченному росту памяти из-за многократных вызовов eval, и как сегодня диагностировать и уменьшить этот эффект.
Процесс Node сразу после запуска выглядит абсолютно здоровым, но по мере продолжения редактирования кода его потребление памяти постепенно растет. Если этот процесс был запущен с параметром --enable-source-maps (или при установленном значении NODE_V8_COVERAGE), и какой-то элемент в стеке вызывает функцию eval с каждым разом с новым значением //# sourceURL, то причина проблемы, скорее всего, кроется в сильно нагруженной кэш-системе источниковых карт, которая накапливает генерируемые записи и никогда их не освобождает. Объем памяти продолжает расти, и при перезапуске процесса он снова увеличивается.
Вы пытаетесь принудительно выполнить сбор мусора, удаляя все возможные ссылки. Однако объем памяти всё равно продолжает расти.
Характер роста
Представьте сервер разработки, который работает в течение длительного времени, или любой процесс Node с долгим сроком жизни, который генерирует искусственные трейсы стека или «кадры владельца», помеченные уникальными URL источников. Показатели RSS и heapUsed постоянно растут. Перезапуск процесса сбрасывает эти показатели, но выполнение той же нагрузки без флага source-map сохраняет уровень памяти на прежнем уровне.
В открытом реестре проблем Node.js, посвященном этому поведению (nodejs/node#65760), минимальный пример воспроизведения предусматривает обработку примерно 90 байт кода с использованием одного внешнего файла source map, при этом на каждой итерации меняется только значение sourceURL. После выполнения принудительной уборки мусора:
Evals | with --enable-source-maps | no flag
0 | 5 MB | 5 MB
400 | 170 MB | 5 MB
800 | 334 MB | 6 MB
1200 | 499 MB | 6 MB
Это означает примерно 415 КБ памяти, сохраняемой при каждом вызове eval, причем верхняя граница не видна.
Если вы работаете с Next.js App Router, это часто проявляется в том, что команда next dev увеличивается на десятки мегабайт при каждой правке файла. Механизм owner-stack в React Server Components выполняет функцию eval один раз на каждую рамку стека, добавляя к каждому вызову метку вида //# sourceURL=about://React/…?<counter++> вместе с большой встроенной картой исходного кода. Поскольку этот счётчик увеличивается при каждом вызове, каждый ключ кэша становится уникальным, поэтому ничего не может быть повторно использовано. Соответствующая тема обсуждения в Next.js — vercel/next.js#98221; рассматривайте её как место, где проявляется симптом, а не как независимую причину.
Почему кэш не отпускает данные
Флаг --enable-source-maps указывает Node на необходимость кэширования источниковых карт, чтобы стек-трейсы во время выполнения могли быть преобразованы обратно в исходные файлы (см. документацию CLI). Источники обычных модулей хранятся в кэше с слабой ключевой связью, поэтому коллектор мусора может освободить их, когда они больше не используются. Однако генерируемые источники — те, которыми занимается ветка isGeneratedSource — попадают в generatedSourceMapCache, что представляет собой обычный Map с прочной ключевой связью, определённый в файле lib/internal/source_map/source_map_cache.js, и из которого они никогда не удаляются.
Комментарий к коду, относящийся к этой кэш-системе, предполагает, что за весь срок работы процесса будет сгенерировано лишь несколько источников кода. Механизмы горячей замены модулей и регенерации стека владельца полностью нарушают это предположение. Каждый уникальный sourceURL становится постоянным ключом, старые записи никогда не удаляются, и полная разобранная карта для каждого из них остается в памяти.
Установка параметра NODE_V8_COVERAGE направляет запрос по точно такому же пути кэша, поэтому вы будете наблюдать такой же неограниченный рост объема кэша каждый раз, когда меняются ключи генерируемых элементов eval.
Уже существует открытый pull request в исходном проекте (#65761), который ограничивает размер кэша generated-sources с использованием стратегии LRU с учетом объема памяти — 32 МБ в последней версии — и обновляет записи при чтении, чтобы карты для активных генерируемых функций не удалялись преждевременно. На данный момент этот PR остается открытым и помечен как needs-ci. Он еще не объединен и не входит ни в одну выпущенную версию Node, поэтому не стоит предполагать, что ваша текущая LTS-версия уже содержит это исправление.
Подтверждение диагноза
- Проверьте, что ваш долгосрочно работающий процесс запущен с параметром
--enable-source-mapsили имеет установленное значениеNODE_V8_COVERAGE, и что какой-то процесс постоянно выполняет командуevalс новым значением//# sourceURLкаждый раз — это может быть HMR, стеки владельцев RSC или пользовательская система генерации кода.
process.memoryUsage().heapUsed после запуска цикла принудительной очистки памяти, либо в отдельном тесте, запущенном с помощью node --expose-gc, либо путем сделания скриншота кучи через инспектор в действующем процессе.generatedSourceMapCache или context:generatedSourceMapCache. Исследователи проблемы обнаружили тысячи сохраненных записей, содержащих более гигабайта данных sourcesContent и mappings во время реальной сессии next dev.Увеличение значения --max-old-space-size не является решением — оно лишь откладывает неизбежный сбой из-за нехватки памяти.
Что делать прямо сейчас
Выберите подход, который подходит вашей среде:
- Для проектов Next.js действуйте немедленно: запустите команду
next dev --disable-source-maps. Согласно данным отчетов по заданию #65760, объем данных значительно снизился — примерно на +6 МБ за каждую правку по сравнению с примерно +89 МБ при включенных картах исходного кода. Читаемость трассировки стек-крашей снижается, но у вашего компьютера больше не возникает проблем с памятью. - Для других инструментов Node, работающих длительное время: удалите параметр
--enable-source-mapsили отключите переменнуюNODE_V8_COVERAGEна любом сервере горячей замены, пока трассировки стек-крашей с картами исходного кода действительно не станут необходимыми. Вместо этого используйте карты исходного кода только во время краткосрочных сессий отладки.
Вывод
Ваш сервер разработки не потребляет память случайным образом без причины. Сочетание параметра --enable-source-maps с многократными вызовами функций eval, использующими уникальные значения sourceURL, приводит к заполнению кэша generatedSourceMapCache, из которого невозможно удалить записи. Обычные карты исходного кода модулей могут быть уничтожены процессом сбора мусора; генерируемые карты — нет, по крайней мере до выхода версии с номером #65761. Выключите карты исходного кода в процессах горячей замены сегодня и планируйте обновление, как только будет выпущен кэш с ограничением объема.
Поэтому в следующий раз, когда вы заметите постепенное увеличение объема памяти RSS во время простого редактирования файлов, а процедура принудительного сбора мусора не поможет этому прекратиться, проверьте, не является ли причиной именно этот флаг.
Связанные материалы
- Компилятор TypeScript на Go и нативная реализация: руководство по миграции — Узнайте, как компилятор TypeScript на основе Go и нативная реализация в Node.js повлияют на кодовые базы React и Next.js, и что необходимо исправить в вашем tsconfig прямо сейчас.
- Замена Jest на нативный тестовый запускатель Node в Node 24 — Пример реальной миграции показывает, как встроенный тестовый запускатель Node 24 и нативная поддержка TypeScript сокращают время выполнения тестов в CI, одновременно устраняя четыре зависимости.