Accueil / Articles / Le cache de la carte source de Node constitue une fuite mémoire silencieuse en mode développement

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.

1073 mots

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

  1. Vérifiez que votre processus en exécution prolongée a été lancé avec --enable-source-maps ou que NODE_V8_COVERAGE est défini, et qu’un élément exécute répétitivement du code à l’aide de eval avec 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é.
  • Exemple de 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.
  • Exécutez la même charge de travail sans le flag. Le signe caractéristique mentionné dans #65760 est que la mémoire reste stable sans ce flag, tandis qu’elle augmente progressivement lorsqu’il est activé.
  • Optionnellement, capturez une snapshot d’heap et recherchez-y 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 :

    1. 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.
    2. Pour les autres outils Node en exécution prolongée : supprimez --enable-source-maps ou désactivez NODE_V8_COVERAGE sur 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.
  • Suivez la correction apportée en amont : suivez à la fois le ticket nodejs/node#65760 et la demande de pull request #65761. Dès qu’une version de Node mentionne explicitement le cache limité des sources générées, il est possible d’effectuer la mise à niveau en toute sécurité. Jusque-là, considérez tous les chiffres fournis par d’autres utilisateurs comme des mesures relatives à leur configuration spécifique, et non comme une garantie que votre application se comportera de la même manière.
  • Évitez le conseil consistant à « simplement augmenter la limite de mémoire ». Le cache sous-jacent reste solide et illimité avec cette combinaison précise de paramètres et de mode d’utilisation. Une limite de mémoire plus élevée ne vous permet que de gagner un peu plus de temps avant la panne.
  • 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