Startseite / Artikel / Caches der Quellkarten von Node stellt im Entwicklungsmodus einen stillen Speicherverlust dar.

Caches der Quellkarten von Node stellt im Entwicklungsmodus einen stillen Speicherverlust dar.

Erfahren Sie, warum das Aktivieren von --enable-source-maps oder NODE_V8_COVERAGE aufgrund wiederholter eval-Aufrufe zu einem unbegrenzten Wachstum des Heap-Speichers führen kann, und wie Sie dies heute diagnostizieren und abmildern können.

1073 Wörter

Der Node-Prozess scheint unmittelbar nach dem Start völlig gesund zu sein, nimmt aber beim weiteren Bearbeiten des Codes allmählich an Speicherbedarf zu. Wenn dieser Prozess mit --enable-source-maps (oder mit gesetztem NODE_V8_COVERAGE) gestartet wurde und etwas in Ihrem Codestack jedes Mal eval mit einer neuen //# sourceURL-Angabe aufruft, haben Sie wahrscheinlich den Übeltäter gefunden: ein stark gefüllter Source-Map-Cache, der generierte Einträge ansammelt und sie niemals freigibt. Der Heap wächst weiter. Sie starten den Prozess neu – er wächst erneut.

Sie versuchen, die Müllsammlung zu zwingen. Sie löschen alle Referenzen, an die Sie denken können. Dennoch steigt der Heap weiter an.

Das Wachstumsmuster

Stellen Sie sich einen Entwicklungsserver vor, der eine Weile weiterläuft, oder generell jeden lang laufenden Node-Prozess, der synthetische Stack-Traces oder „Owner Frames“ erzeugt, die mit eindeutigen Quell-URLs versehen sind. Sowohl RSS als auch heapUsed steigen die ganze Zeit an. Das Neustarten des Prozesses setzt den Verlauf neu, doch das Ausführen derselben Arbeitslast ohne die source-map-Flag lässt den Speicherverbrauch konstant.

In der öffentlichen Node.js-Issue-Tracking-Seite zu diesem Verhalten (nodejs/node#65760) wird eine minimale Replikation durchgeführt, bei der etwa 90 Bytes Quellcode gegenüber einer einzigen externen Source Map bewertet werden, wobei bei jeder Iteration nur die sourceURL geändert wird. Nach Durchführung der erzwungenen Garbage Collection:

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

Das ergibt in etwa 415 KB, die pro eval-Aufruf behalten bleiben, wobei kein Obergrenzen sichtbar sind.

Falls Sie mit dem Next.js App Router arbeiten, zeigt sich dies oft darin, dass next dev bei jeder Dateiänderung um Dutzende Megabyte an Größe zunimmt. Das Owner-Stack-Mechanismus der React Server Components führt eval einmal pro Stack-Frame aus und kennzeichnet jeden Aufruf mit etwas wie //# sourceURL=about://React/…?<counter++> zusammen mit einer großen, eingebetteten Quellkartei. Da dieser Zähler bei jedem Aufruf inkrementiert wird, ist jeder Cache-Schlüssel einzigartig – daher wird nichts wiederverwendet. Der entsprechende Diskussionsthread in Next.js ist vercel/next.js#98221 – betrachten Sie ihn als den Ort, an dem das Problem auftritt, nicht als eine unabhängige Ursache.

Warum der Cache die Daten nicht freigibt

Die Flagge --enable-source-maps weist Node an, Source Maps zu cachen, damit Laufzeit-Stacktraces in die ursprünglichen Quelldateien übersetzt werden können (siehe die CLI-Dokumentation). Reguläre Modulquellen werden über einen schwach gesperrten Cache abgelegt, sodass der Garbage Collector sie zurückgewinnen kann, sobald sie nicht mehr referenziert werden. Generierte Quellen hingegen – jene, die durch den isGeneratedSource-Branch verarbeitet werden – landen in generatedSourceMapCache, einem einfachen, stark referenzierten Map, der in lib/internal/source_map/source_map_cache.js definiert ist und niemals bereinigt wird.

Der Kommentar im Code bezüglich dieses Caches geht davon aus, dass es während der Lebensdauer eines Prozesses nur wenige generierte Quellen geben wird. Hot Module Replacement sowie die Regeneration des Owner-Stacks durchbrechen diese Annahme völlig. Jeder eindeutige sourceURL wird zu einer dauerhaften Schlüsselwerte, alte Einträge werden niemals gelöscht, und die vollständig analysierte Karte für jeden Eintrag bleibt im Speicher gespeichert.

Durch die Einstellung von NODE_V8_COVERAGE wird genau derselbe Cache-Pfad genutzt, wodurch Sie ebenfalls ein unbegrenztes Wachstum beobachten werden, solange sich Ihre generierten eval-Schlüssel ständig ändern.

Es gibt bereits einen offenen Pull Request upstream (#65761), der den Cache für generated-sources mit einer LRU-Strategie basierend auf einem Byte-Budget begrenzt – in der neuesten Revision auf 32 MiB – und die Einträge bei jeder Lektüre aktualisiert, damit Karten für weiterhin aktive generierte Funktionen nicht vorzeitig entfernt werden. Derzeit bleibt dieser PR offen und ist mit needs-ci markiert. Er wurde noch nicht eingemergt und ist auch nicht Teil einer veröffentlichten Node-Build-Version, daher sollten Sie nicht annehmen, dass Ihre aktuelle LTS-Version bereits diesen Fix enthält.

Diagnose bestätigen

  1. Überprüfen Sie, ob Ihr langlaufender Prozess mit --enable-source-maps gestartet wurde oder ob NODE_V8_COVERAGE gesetzt ist, und ob etwas immer wieder Code mit einer neuen //# sourceURL ausführt – dies könnte HMR, RSC-Owner-Stacks oder benutzerdefinierte Codegenerierung sein.
  • Nehmen Sie einen Wert von process.memoryUsage().heapUsed nach Ausführung einer Schleife mit erzwungenem GC auf, entweder in einer eigenständigen Wiederholung unter Verwendung von node --expose-gc oder durch Erstellung eines Heap-Snapsshots über den Inspektor im laufenden Prozess.
  • Führen Sie dieselbe Arbeitslast ohne das entsprechende Flag aus. Das charakteristische Merkmal gemäß #65760 ist, dass der Speicherverbrauch ohne das Flag konstant bleibt, während er mit aktiviertem Flag stetig ansteigt.
  • Optional können Sie einen Heap-Snapshot erstellen und darin nach generatedSourceMapCache oder context:generatedSourceMapCache suchen. Die Berichterstatter des Problems fanden während einer echten next dev-Sitzung Tausende von gespeicherten Einträgen, die insgesamt mehr als einen Gigabyte an sourcesContent- und mappings-Daten enthielten.
  • Die Erhöhung von --max-old-space-size ist keine Lösung – sie verschiebt lediglich den unvermeidlichen Absturz aufgrund von Speicherengpässen.

    Was Sie jetzt tun sollten

    Wählen Sie den Ansatz, der zu Ihrer Konfiguration passt:

    1. Für Next.js-Projekte: Handeln Sie umgehend: Führen Sie next dev --disable-source-maps aus. Berichterstatter im Thread #65760 stellten fest, dass der Speicherverbrauch deutlich abnahm – etwa +6 MB pro Änderung im Vergleich zu rund +89 MB bei aktivierten Quellkarten, basierend auf ihren Messungen. Die Lesbarkeit der Stack-Traces nimmt zwar etwas ab, doch Ihr Rechner läuft nicht mehr aus dem Speicher heraus.
    2. Für andere langlaufende Node-Tools: Entfernen Sie --enable-source-maps oder deaktivieren Sie NODE_V8_COVERAGE auf jedem Hot-Reloading-Server, solange tatsächlich mappierte Stack-Traces erforderlich sind. Verwenden Sie Quellkarten stattdessen für kurze Debugging-Sitzungen.
  • Verfolgen Sie die Korrektur im Quellcode: Behalten Sie sowohl nodejs/node#65760 als auch den Pull Request #65761 im Auge. Sobald eine Node-Version ausdrücklich auf den begrenzten Cache für generierte Quellen verweist, ist ein Upgrade sicher. Bis dahin sollten Sie alle Angaben anderer Nutzer als Messwerte aus deren spezifischer Konfiguration betrachten und nicht als Garantie dafür, dass Ihre Anwendung sich genauso verhält.
  • Ignorieren Sie den Ratschlag „Erhöhen Sie einfach die Heap-Limit“. Der zugrunde liegende Cache ist bei dieser genauen Kombination aus Flags und Nutzungsmuster stark und unbegrenzt. Eine höhere Heap-Obergrenze verschafft Ihnen nur etwas mehr Zeit, bevor es zu einem Absturz kommt.
  • Fazit

    Ihr Entwicklungsserver verbraucht nicht willkürlich Speicher ohne Grund. Die Kombination von --enable-source-maps mit wiederholten Auswertungen, die eindeutige sourceURL-Werte verwenden, füllt einen stark referenzierten generatedSourceMapCache, der seine Einträge niemals freigibt. Reguläre Modul-Quellkarten können gecacht und später gelöscht werden; generierte Quellkarten hingegen nicht, zumindest bis #65761 in einer Version veröffentlicht wird. Schalten Sie Quellkarten bei Hot-Reload-Prozessen vorerst aus und planen Sie einen Upgrade, sobald der begrenzte Cache verfügbar ist.

    Sollten Sie also das nächste Mal feststellen, dass der RSS-Wert beim einfachen Bearbeiten von Dateien ansteigt und eine erzwungene Garbage Collection nichts dagegen ausrichten kann, prüfen Sie, ob dieser Flag der Grund dafür ist.

    Verwandte Artikel