Pamięć cache mapy źródłowej węzła stanowi cichą wyciek pamięci w trybie deweloperskim
Dowiedz się, dlaczego włączenie opcji --enable-source-maps lub NODE_V8_COVERAGE może powodować niekontrolowane rozrastanie się pamięci heap wskutek powtarzających się wywołań eval, oraz jak można to zdiagnozować i złagodzić już dziś.
Po uruchomieniu proces Node wydaje się w pełni sprawny, ale stopniowo zajmuje coraz więcej pamięci w miarę edycji kodu. Jeśli proces został uruchomiony z opcją --enable-source-maps (lub przy ustawionym NODE_V8_COVERAGE) i coś w Twoim kodzie ciągle wywołuje funkcję eval z nowym tagiem //# sourceURL za każdym razem, prawdopodobnie znalazłeś przyczynę problemu: silny cache map źródłowych, który gromadzi generowane wpisy i nigdy ich nie usuwa. Pamięć heap stale rośnie, więc restartujesz proces – on znów się powiększa.
Próbujesz zmusić do przeprowadzenia zbierania śmieci. Usuwasz wszystkie możliwe odniesienia, ale pamięć heap i tak nadal rośnie.
Wzorzec wzrostu
Załóżmy serwer deweloperski działający przez jakiś czas lub dowolny proces Node trwający długo, który regeneruje syntetyczne ślady stosu lub „ramki właściciela” oznaczone unikalnymi adresem źródłowymi. Zarówno wartość RSS, jak i heapUsed stale rosną. Ponowne uruchomienie procesu sformatuje tę krzywą, ale wykonywanie tego samego obciążenia bez flagi source-map utrzymuje pamięć na stałym poziomie.
W publicznym systemie śledzenia problemów Node.js dotyczącym tego zachowania (nodejs/node#65760) minimalny przykład reprodukcji obejmuje około 90 bajtów kodu źródłowego przetwarzanych przy użyciu jednego zewnętrznego mapowania źródłowego, przy czym przy każdej iteracji zmieniana jest tylko wartość sourceURL. Po przeprowadzeniu przymusowej kolekcji śmieci:
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
Oznacza to w przybliżeniu 415 KB pamięci zachowanej na każde wezwanie eval, przy czym nie widać żadnego górnego ograniczenia.
Jeśli pracujesz z Next.js App Router, problem ten często objawia się w postaci tego, że next dev zyskuje dziesiątki megabajtów przy każdej edycji pliku. Mechanizm owner-stack w React Server Components uruchamia eval raz na każdą ramę stosu, oznaczając każde wywołanie czymś w rodzaju //# sourceURL=about://React/…?<counter++> wraz z dużym mapowaniem źródłowym wplecionym bezpośrednio. Ponieważ ten licznik wzrasta przy każdym wywołaniu, każdy klucz cache’a jest unikalny, więc nic nie jest ponownie wykorzystywane. Odpowiednia dyskusja na Next.js znajduje się pod adresem vercel/next.js#98221 – należy ją traktować jako miejsce, w którym pojawia się ten objaw, a nie jako odrębną przyczynę.
Dlaczego cache nie chce zwolnić
Flaga --enable-source-maps instruuje Node do przechowywania w pamięci cache map źródłowych, aby ślady stosu z czasu wykonywania mogły zostać przetłumaczone z powrotem na oryginalne pliki źródłowe (zobacz dokumentację CLI). Zwykłe źródła modułów przechodzą przez cache o słabych kluczach, dzięki czemu kolektor śmieci może je zwolnić, gdy przestaną być referencjonowane. Natomiast źródła wygenerowane — te obsługiwane przez gałąź isGeneratedSource — trafiają do generatedSourceMapCache, czyli zwykłego, silnie referencjonowanego obiektu Map zdefiniowanego w pliku lib/internal/source_map/source_map_cache.js, który nigdy nie jest oczyszczany.
Komentarz w kodzie dotyczący tego cache zakłada, że w ciągu trwania procesu będzie istniało zaledwie kilka wygenerowanych źródeł. Funkcje Hot Module Replacement oraz regeneracja stosu właściciela całkowicie obalają to założenie. Każdy unikalny sourceURL staje się trwałym kluczem, stare wpisy nigdy nie są usuwane, a pełna mapa zanalizowana dla każdego z nich pozostaje w pamięci.
Ustawienie NODE_V8_COVERAGE kieruje żądania tą samą ścieżką cache, więc zobaczysz ten sam nieograniczony wzrost, gdy klucze wygenerowane przez eval będą się ciągle zmieniać.
Istnieje już otwarty pull request w głównym repozytorium (#65761), który ogranicza bufor generated-sources za pomocą strategii LRU z wykorzystaniem limitu bajtów — 32 MiB w najnowszej wersji — oraz odświeża wpisy podczas odczytu, aby mapy funkcji nadal aktywnych nie były wyrzucane przedwcześnie. Na chwilę obecną ten PR pozostaje otwarty i oznaczony jako needs-ci. Nie został połączony i nie jest częścią żadnej wydanej wersji Node, więc nie zakładaj, że twoja obecna wersja LTS już zawiera to rozwiązanie.
Potwierdzenie diagnozy
- Sprawdź, czy twój proces działający przez długi czas został uruchomiony z parametrem
--enable-source-mapslub ma ustawioneNODE_V8_COVERAGE, oraz czy coś powtarzalnie wykonywa operacjęevalna kodzie z nowym tagiem//# sourceURLza każdym razem — może to być HMR, stosy właścicieli RSC lub niestandardowe generatory kodu.
process.memoryUsage().heapUsed po uruchomieniu pętli z przymusowym GC, zarówno w oddzielnej sesji reprodukcji rozpoczętej za pomocą node --expose-gc, jak i poprzez zrobienie zdjęcia pamięci heap za pomocą inspektora w działającym procesie.generatedSourceMapCache lub context:generatedSourceMapCache. Osoby badające ten problem znalazły tysiące zapisów, które przechowywały łącznie ponad gigabajt danych sourcesContent i mappings podczas rzeczywistej sesji next dev.Zwiększenie wartości --max-old-space-size nie jest rozwiązaniem — jedynie opóźnia ostateczny awarię spowodowaną brakiem pamięci.
Co robić natychmiast
Wybierz metodę odpowiednią dla Twojego środowiska:
- Dla projektów Next.js, działaj natychmiast: uruchom
next dev --disable-source-maps. Osoby sprawdzające problem na #65760 zauważyły znaczne zmniejszenie wzrostu rozmiaru plików — około +6 MB przy każdej modyfikacji w porównaniu z około +89 MB przy włączonych mapach źródłowych, według ich pomiarów. Tracisz nieco czytelność ścieżek wywołań, ale komputer przestaje mieć problemy z pamięcią. - Dla innych narzędzi Node działających długo: usuń
--enable-source-mapslub wyłączNODE_V8_COVERAGEna dowolnym serwerze hot-reloading, dopóki mapowanie ścieżek wywołań nie stanie się faktycznie konieczne. Zamiast tego przeznacz mapowanie źródłowe na krótkotrwałe sesje debugowania.
Wniosek
Twój serwer deweloperski nie zużywa pamięci losowo bez powodu. Połączenie flagi --enable-source-maps z wielokrotnymi operacjami eval wykorzystującymi unikalne wartości sourceURL powoduje wypełnienie silnie referencowanego bufora generatedSourceMapCache, który nigdy nie uwalnia swoich wpisów. Zwykłe mapy źródłowe modułów mogą zostać poddane procesowi zbierania śmieci; te wygenerowane nie mogą, przynajmniej dopóki numer #65761 nie trafi do kolejnej wersji oprogramowania. Wyłącz mapy źródłowe podczas procesów hot-reload już dziś i planuj aktualizację, gdy zostanie wdrożony ograniczony bufor.
Zatem następnym razem, gdy zauważysz wzrost wartości RSS podczas prostego edytowania plików, a przymusowe zbieranie śmieci nic nie daje efektu, sprawdź, czy to właśnie ta flaga jest przyczyną.
Literatura pokrewna
- Kompilator TypeScript w Go i wykonywanie natywne: Przewodnik po migracji — Dowiedz się, jak kompilator TypeScript oparty na Go oraz wykonywanie natywne w Node.js wpłyną na projekty React i Next.js oraz co należy natychmiast poprawić w pliku tsconfig.
- Zamiana Jest na natywnego wykonywacza testow w Node 24 — Przykład z praktyki pokazuje, jak wbudowany wykonywacz testow w Node 24 oraz natywna obsługa TypeScript skracają czas CI, jednocześnie eliminując cztery zależności.