Inicio / Artículos / El caché de mapas de código de Node es una fuga de memoria silenciosa en modo desarrollo.

El caché de mapas de código de Node es una fuga de memoria silenciosa en modo desarrollo.

Aprenda por qué habilitar --enable-source-maps o NODE_V8_COVERAGE puede causar un crecimiento ilimitado del heap debido a llamadas repetidas de eval, y cómo diagnosticarlo y mitigarlo en la actualidad.

1073 palabras

El proceso de Node parece estar perfectamente funcional justo después del inicio, pero su consumo de memoria aumenta gradualmente a medida que se sigue editando el código. Si ese proceso se inició con --enable-source-maps (o con NODE_V8_COVERAGE activado), y algo en su estructura sigue llamando a eval con un nuevo //# sourceURL cada vez, es probable que hayan encontrado la causa: un caché de mapas de fuente muy grande que acumula entradas generadas y nunca las libera. El heap sigue creciendo. Se reinicia el proceso, y vuelve a crecer.

Prueban forzar la recolección de basura. Eliminan todas las referencias que se les ocurren. De todos modos, el heap sigue aumentando.

El patrón de crecimiento

Imagínese un servidor de desarrollo que sigue en ejecución durante un rato, o cualquier proceso Node de larga duración que genere registros de pila sintéticos o “frames del propietario” etiquetados con URLs de origen únicas. Tanto RSS como heapUsed aumentan constantemente. Reiniciar el proceso restablece la curva, pero ejecutar la misma carga de trabajo sin la opción source-map mantiene la memoria estable.

En el registro público de problemas de Node.js relacionado con este comportamiento (nodejs/node#65760), una reproducción mínima evalúa aproximadamente 90 bytes de código fuente contra un único mapa de origen externo, cambiando solo el sourceURL en cada iteración. Después de ejecutar la recolección de basura forzada:

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

Eso equivale a aproximadamente 415 KB retenidos por cada llamada a eval, sin que se vea un límite superior.

Si trabajas con el Next.js App Router, esto suele manifestarse en que next dev aumenta en decenas de megabytes con cada edición de archivo. El mecanismo owner-stack de React Server Components ejecuta eval una vez por marco del stack, etiquetando cada llamada con algo como //# sourceURL=about://React/…?<counter++> junto con un gran mapa de fuentes incrustado. Dado que ese contador aumenta en cada llamada, cada clave de caché es única, por lo que nada se reutiliza nunca. El hilo de discusión correspondiente en Next.js es vercel/next.js#98221; considéralo el lugar donde se desencadena el síntoma, y no una causa independiente.

Por qué la caché no libera los datos

La bandera --enable-source-maps indica a Node que almacene en caché los mapas de fuente para que las trazas de pila en tiempo de ejecución puedan ser traducidas nuevamente a los archivos de fuente originales (consulte la documentación de la CLI). Las fuentes de módulos normales pasan por un caché con claves débiles, por lo que el recolector de basura puede recuperarlas una vez que ya no se refieren a ellas. Sin embargo, las fuentes generadas —aquellas gestionadas por la rama isGeneratedSource— terminan en generatedSourceMapCache, un Map simple con referencias fuertes definido en lib/internal/source_map/source_map_cache.js que nunca se elimina.

El comentario del código relacionado con ese caché asume que habrá solo un pequeño número de fuentes generadas a lo largo de la vida útil de un proceso. El reemplazo dinámico de módulos y la regeneración del stack del propietario rompen por completo esa suposición. Cada sourceURL distinto se convierte en una clave permanente, nunca se eliminan las entradas antiguas, y el mapa completo analizado para cada una permanece en memoria.

Al establecer NODE_V8_COVERAGE, se sigue exactamente el mismo camino del caché, por lo que observarás el mismo crecimiento ilimitado cada vez que las claves de evaluación generadas sigan cambiando.

Ya existe una solicitud de pull abierta en el repositorio principal (#65761) que limita la caché de generated-sources mediante una estrategia LRU basada en un presupuesto de bytes: 32 MiB en la revisión más reciente. Además, actualiza los registros al leerlos para que los mapas de las funciones generadas aún activas no sean eliminados prematuramente. Por el momento, dicha solicitud sigue abierta y marcada como needs-ci. No ha sido fusionada ni forma parte de ninguna versión publicada de Node, por lo que no asuma que su versión LTS actual ya incluye esta corrección.

Confirmación del diagnóstico

  1. Verifique que su proceso de ejecución prolongada se inició con --enable-source-maps o que tenga configurado NODE_V8_COVERAGE, y que algo esté evaluando repetidamente código con un nuevo //# sourceURL cada vez; esto podría ser HMR, pilas de propietario de RSC o generación de código personalizada.
  • Ejemplo de process.memoryUsage().heapUsed después de ejecutar un bucle de recolección de basura forzada, ya sea en una reproducción independiente iniciada con node --expose-gc, o tomando una captura del heap a través del inspector en el proceso en ejecución.
  • Ejecuta la misma carga de trabajo sin la bandera activada. La señal distintiva del caso #65760 es que la memoria se mantiene estable sin dicha bandera y aumenta constantemente cuando está activada.
  • Opcionalmente, toma una captura del heap y busca en ella generatedSourceMapCache o context:generatedSourceMapCache. Los investigadores del problema encontraron miles de entradas retenidas que albergaban más de un gigabyte en conjunto de datos sourcesContent y mappings durante una sesión real de next dev.
  • Aumentar el valor de --max-old-space-size no es una solución; solo pospone el fallo eventual por falta de memoria.

    Qué hacer ahora mismo

    Elija el enfoque que se adapte a su configuración:

    1. Para proyectos Next.js, actúe de inmediato: ejecute next dev --disable-source-maps. Los informantes en #65760 observaron que el aumento de tamaño se redujo significativamente: aproximadamente +6 MB por edición en comparación con unos +89 MB cuando estaban activas las mapas de fuente, según sus mediciones. Perderá algo en la legibilidad de los rastros de ejecución, pero su equipo dejará de quedarse sin memoria.
    2. Para otras herramientas Node que funcionan durante mucho tiempo: elimine --enable-source-maps o desactive NODE_V8_COVERAGE en cualquier servidor de recarga en tiempo real hasta que los rastros de ejecución mapeados sean realmente necesarios. Reserve las mapas de fuente para sesiones de depuración de corta duración.
  • Seguir la solución en el origen: haga seguimiento tanto de nodejs/node#65760 como de la solicitud de pull #65761. Una vez que una versión de Node mencione explícitamente el caché limitado de generated-sources, será seguro realizar la actualización. Hasta entonces, considere cualquier número proporcionado por otros informantes como una medida de su configuración específica, y no como una garantía de que su aplicación se comportará de manera idéntica.
  • Omitir el consejo de “simplemente aumentar el límite del heap”. El caché subyacente es sólido e ilimitado bajo esta combinación exacta de parámetros y patrón de uso. Un límite del heap más alto solo le brinda un poco más de tiempo antes de que ocurra la caída.
  • Conclusión

    Tu servidor de desarrollo no está consumiendo memoria al azar sin motivo. Combinar --enable-source-maps con evaluaciones repetidas que utilizan valores únicos de sourceURL llena un generatedSourceMapCache con referencias intensas que nunca libera sus entradas. Los mapas de fuente de módulos normales pueden ser recogidos como basura; los generados no, al menos hasta que se incluya la versión #65761 en una actualización. Desactiva los mapas de fuente en los procesos de recarga en tiempo real hoy mismo, y planea hacer la actualización una vez que esté disponible el caché con límites.

    Así que la próxima vez que notes que el uso de memoria RSS aumenta mientras solo estás editando archivos, y que la recogida forzada de basura no hace nada para detenerlo, verifica si esa bandera es la causa.

    Lecturas relacionadas