From 96860f39728edd36d5efa8483744386196f555f3 Mon Sep 17 00:00:00 2001 From: Sam Attard Date: Fri, 12 Jun 2026 17:50:49 +0000 Subject: [PATCH] module: avoid allocating a cache key string for every require() The relative resolve cache was keyed by a concatenated ${parent.path}\x00${request} string, allocating a new key for every require() call including fully cached ones. Key the cache by the parent directory first (a Map keyed by the already-retained module.path string) and then by the request (a dictionary object, whose property access internalizes dynamically-constructed specifiers). Faster on every measured workload shape and slightly smaller in memory, since the concatenated keys are no longer retained. Signed-off-by: Sam Attard --- lib/internal/modules/cjs/loader.js | 34 ++++++++++++++++++------------ 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 3d172f75fe82da..3d1b0d43b2fb3d 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -223,7 +223,7 @@ let { startTimer, endTimer } = debugWithTimer('module_timer', (start, end) => { const { tracingChannel } = require('diagnostics_channel'); const onRequire = getLazy(() => tracingChannel('module.require')); -const relativeResolveCache = { __proto__: null }; +const relativeResolveCache = new SafeMap(); let requireDepth = 0; let isPreloading = false; @@ -1273,17 +1273,23 @@ function loadBuiltinWithHooks(id, url, format) { * @returns {object} */ Module._load = function(request, parent, isMain, internalOptions = kEmptyObject) { - let relResolveCacheIdentifier; + let relResolveCacheByDir; if (parent) { debug('Module._load REQUEST %s parent: %s', request, parent.id); - // Fast path for (lazy loaded) modules in the same directory. The indirect - // caching is required to allow cache invalidation without changing the old - // cache key names. - relResolveCacheIdentifier = `${parent.path}\x00${request}`; - const filename = relativeResolveCache[relResolveCacheIdentifier]; - reportModuleToWatchMode(filename); - reportModuleToWatchModeFromWorker(filename); + // Fast path for (lazy loaded) modules in the same directory. Keyed by + // parent directory and then request, so no concatenated cache key + // string is allocated per require() call. + relResolveCacheByDir = relativeResolveCache.get(parent.path); + if (relResolveCacheByDir === undefined) { + // A plain object handles dynamically built specifier strings + // better than a Map here. + relResolveCacheByDir = { __proto__: null }; + relativeResolveCache.set(parent.path, relResolveCacheByDir); + } + const filename = relResolveCacheByDir[request]; if (filename !== undefined) { + reportModuleToWatchMode(filename); + reportModuleToWatchModeFromWorker(filename); const cachedModule = Module._cache[filename]; if (cachedModule !== undefined) { updateChildren(parent, cachedModule, true); @@ -1292,7 +1298,7 @@ Module._load = function(request, parent, isMain, internalOptions = kEmptyObject) } return cachedModule.exports; } - delete relativeResolveCache[relResolveCacheIdentifier]; + delete relResolveCacheByDir[request]; } } @@ -1395,8 +1401,8 @@ Module._load = function(request, parent, isMain, internalOptions = kEmptyObject) module[kFormat] ??= format; } - if (parent !== undefined) { - relativeResolveCache[relResolveCacheIdentifier] = filename; + if (relResolveCacheByDir !== undefined) { + relResolveCacheByDir[request] = filename; } let threw = true; @@ -1407,7 +1413,9 @@ Module._load = function(request, parent, isMain, internalOptions = kEmptyObject) if (threw) { delete Module._cache[filename]; if (parent !== undefined) { - delete relativeResolveCache[relResolveCacheIdentifier]; + if (relResolveCacheByDir !== undefined) { + delete relResolveCacheByDir[request]; + } const children = parent?.children; if (ArrayIsArray(children)) { const index = ArrayPrototypeIndexOf(children, module);