Le cache de la carte source de Node constitue une fuite mémoire silencieuse en mode développement
Découvrez pourquoi l’activation de --enable-source-maps ou de NODE_V8_COVERAGE peut provoquer une croissance illimitée du heap en raison de appels répétés à eval, ainsi que comment le diagnostiquer et l’atténuer dès aujourd’hui.
Votre processus Node semble parfaitement en bon état juste après le démarrage, mais sa consommation de mémoire augmente progressivement à mesure que vous continuez d’éditer du code. Si ce processus a été lancé avec --enable-source-maps (ou avec NODE_V8_COVERAGE activé), et que quelque chose dans votre pile d’appels continue d’utiliser eval avec un nouveau //# sourceURL à chaque fois, vous avez probablement trouvé la cause : un cache de cartes sources très volumineux qui accumule les entrées générées sans jamais les libérer. La mémoire allouée continue de croître. Vous redémarrez le processus : elle augmente à nouveau.
Vous essayez de forcer le nettoyage automatique des données inutilisées. Vous supprimez toutes les références dont vous pensez pouvoir vous passer. La consommation de mémoire continue malgré tout d’augmenter.
Le schéma de croissance
Imaginez un serveur de développement laissé en marche pendant un certain temps, ou tout processus Node à longue durée de vie qui génère des traces d’erreur synthétiques ou des « frames propriétaires » marqués avec des URL sources uniques. Tant les valeurs RSS que heapUsed augmentent constamment. Redémarrer le processus réinitialise cette courbe, mais exécuter la même charge de travail sans l’option source-map maintient la mémoire à un niveau constant.
Dans le suivi public des problèmes Node.js concernant ce comportement (nodejs/node#65760), une reproduction minimale évalue environ 90 octets de code source à l’aide d’une seule carte de sources externe, en modifiant uniquement la valeur sourceURL à chaque itération. Après exécution d’une collecte de déchets forcée :
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
Cela correspond à environ 415 KB conservés par appel à eval, sans limite apparente.
Si vous travaillez avec le Next.js App Router, ce problème se manifeste souvent par l’augmentation de la taille de next dev de dizaines de mégaoctets à chaque modification de fichier. Le mécanisme owner-stack des React Server Components exécute eval une fois par cadre de stack, en annotant chaque appel avec quelque chose comme //# sourceURL=about://React/…?<counter++> ainsi qu’avec une grande carte de sources intégrée. Comme ce compteur augmente à chaque appel, chaque clé de cache est unique, ce qui empêche tout réutilisement. Le fil de discussion correspondant sur Next.js se trouve à vercel/next.js#98221 — considérez-le comme le lieu où ce symptôme apparaît, et non comme une cause indépendante.
Pourquoi le cache refuse de libérer les données
Le flag --enable-source-maps indique à Node de mettre en cache les Source Maps afin que les traces d’erreur en temps de exécution puissent être traduites vers vos fichiers sources originaux (voir la documentation CLI). Les sources de modules ordinaires passent par un cache à clés faibles, ce qui permet au collecteur de déchets de les récupérer une fois qu’elles ne sont plus référencées. En revanche, les sources générées — celles gérées par la branche isGeneratedSource — se retrouvent dans generatedSourceMapCache, un Map simple à référencement fort défini dans lib/internal/source_map/source_map_cache.js qui n’est jamais purgé.
Les commentaires du code concernant ce cache partent du principe qu’il n’y aura que quelques sources générées au cours de la durée de vie d’un processus. Le remplacement en temps réel des modules et la régénération de la pile d’exécution viennent complètement contredire cette hypothèse. Chaque sourceURL distinct devient une clé permanente, aucune entrée ancienne n’est jamais supprimée, et la carte complète analysée pour chacune reste en mémoire.
La définition de NODE_V8_COVERAGE emprunte exactement le même chemin de cache, ce qui entraîne une croissance illimitée similaire chaque fois que les clés d’évaluation générées changent.
Il existe déjà une demande de fusion ouverte en amont (#65761) qui limite le cache generated-sources à l’aide d’une stratégie LRU basée sur un budget en octets — 32 Mo dans la dernière version — et met à jour les entrées lors de chaque lecture afin que les cartes des fonctions générées encore actives ne soient pas éliminées prématurément. Pour l’instant, cette demande reste ouverte et est marquée needs-ci. Elle n’a pas encore été fusionnée et ne fait pas partie d’aucune version Node publiée, donc ne supposez pas que votre version LTS actuelle inclut déjà cette correction.
Confirmation du diagnostic
- Vérifiez que votre processus en exécution prolongée a été lancé avec
--enable-source-mapsou queNODE_V8_COVERAGEest défini, et qu’un élément exécute répétitivement du code à l’aide deevalavec un nouveau//# sourceURLà chaque fois — il s’agit peut-être de HMR, des piles d’auteur de RSC ou d’un générateur de code personnalisé.
process.memoryUsage().heapUsed après l’exécution d’une boucle de GC forcé, que ce soit dans une reproduction indépendante lancée avec node --expose-gc, ou en prenant une capture d’heap via l’inspecteur sur votre processus en cours d’exécution.generatedSourceMapCache ou context:generatedSourceMapCache. Les rapporteurs de ce problème ont découvert des milliers d’entrées conservées contenant plus d’un gigaoctet de données combinées sourcesContent et mappings lors d’une session réelle de next dev.Augmenter la valeur de --max-old-space-size n’est pas une solution — cela ne fait que reporter l’erreur de manque de mémoire à un moment ultérieur.
Que faire immédiatement
Choisissez l’approche qui convient à votre configuration :
- Pour les projets Next.js, agissez sans tarder : exécutez
next dev --disable-source-maps. Selon les mesures effectuées par les rapporteurs sur #65760, la consommation de mémoire diminue considérablement — d’environ +6 MB par modification contre plus de +89 MB lorsque les cartes sources sont activées. Vous perdez un peu en lisibilité des traces d’erreur, mais votre machine n’est plus confrontée à une épuisement de mémoire. - Pour les autres outils Node en exécution prolongée : supprimez
--enable-source-mapsou désactivezNODE_V8_COVERAGEsur tout serveur de chargement dynamique jusqu’à ce que les traces d’erreur mappées soient réellement nécessaires. Préservez plutôt l’utilisation des cartes sources pour des sessions de débogage de courte durée.
En résumé
Votre serveur de développement n’utilise pas aléatoirement de la mémoire sans raison. En combinant --enable-source-maps avec des appels répétés d’eval utilisant des valeurs uniques pour sourceURL, on remplit un generatedSourceMapCache fortement référencé qui ne libère jamais ses entrées. Les cartes de source des modules ordinaires peuvent faire l’objet d’un collecte de déchets ; celles générées ne le peuvent pas, du moins pas avant que la version #65761 ne soit publiée. Désactivez les cartes de source lors des processus de hot-reload dès aujourd’hui, et prévoyez une mise à niveau une fois que le cache limité sera disponible.
Ainsi, la prochaine fois que vous remarquerez que l’usage de mémoire RSS augmente progressivement alors que vous modifiez simplement des fichiers, et que la collecte de déchets forcée ne parvient pas à l’arrêter, vérifiez si c’est ce paramètre en cause.
Lectures complémentaires
- Le compilateur Go de TypeScript et l’exécution native : un guide de migration — Découvrez comment le compilateur basé sur Go de TypeScript et l’exécution native via Node.js affectent les bases de code React et Next.js, ainsi que ce qu’il convient de corriger dans votre tsconfig dès maintenant.
- Remplacer Jest par l’exécuteur de tests natif de Node dans Node 24 — Une migration concrète montre comment l’exécuteur de tests intégré à Node 24 et le support natif de TypeScript réduisent le temps de CI tout en éliminant quatre dépendances.