Commit 471fe813bb3 for nodejs

commit 471fe813bb38c687a1bde205d98f7750c23a664e
Author: Samuel Attard <sattard@anthropic.com>
Date:   Mon Sep 28 16:30:13 2026 -0700

    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 <sattard@anthropic.com>
    PR-URL: https://github.com/nodejs/node/pull/63884
    Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
    Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>

diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js
index 1995eef579e..2869abba869 100644
--- a/lib/internal/modules/cjs/loader.js
+++ b/lib/internal/modules/cjs/loader.js
@@ -229,7 +229,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;
@@ -1339,17 +1339,19 @@ function loadBuiltinWithHooks(id, url, format) {
  * @returns {object}
  */
 Module._load = function(request, parent, isMain, internalOptions = kEmptyObject) {
-  let relResolveCacheIdentifier;
+  let parentPath;
+  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.
+    parentPath = parent.path;
+    relResolveCacheByDir = relativeResolveCache.get(parentPath);
+    const filename = relResolveCacheByDir?.[request];
     if (filename !== undefined) {
+      reportModuleToWatchMode(filename);
+      reportModuleToWatchModeFromWorker(filename);
       const cachedModule = Module._cache[filename];
       if (cachedModule !== undefined) {
         updateChildren(parent, cachedModule, true);
@@ -1358,7 +1360,7 @@ Module._load = function(request, parent, isMain, internalOptions = kEmptyObject)
         }
         return cachedModule.exports;
       }
-      delete relativeResolveCache[relResolveCacheIdentifier];
+      delete relResolveCacheByDir[request];
     }
   }

@@ -1461,8 +1463,16 @@ Module._load = function(request, parent, isMain, internalOptions = kEmptyObject)
     module[kFormat] ??= format;
   }

-  if (parent !== undefined) {
-    relativeResolveCache[relResolveCacheIdentifier] = filename;
+  if (parent) {
+    // Only create the per-directory bucket once there is an entry to store,
+    // so builtins and failed resolutions don't leave empty buckets behind.
+    if (relResolveCacheByDir === undefined) {
+      // A plain object handles dynamically built specifier strings
+      // better than a Map here.
+      relResolveCacheByDir = { __proto__: null };
+      relativeResolveCache.set(parentPath, relResolveCacheByDir);
+    }
+    relResolveCacheByDir[request] = filename;
   }

   let threw = true;
@@ -1473,7 +1483,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);
diff --git a/test/parallel/test-module-relative-resolve-cache.js b/test/parallel/test-module-relative-resolve-cache.js
new file mode 100644
index 00000000000..b9868a3a3c9
--- /dev/null
+++ b/test/parallel/test-module-relative-resolve-cache.js
@@ -0,0 +1,58 @@
+'use strict';
+
+// Checks that Module._load() only creates a relative resolve cache entry for
+// the parent directory when there is a resolved filename to store in it.
+
+require('../common');
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const Module = require('module');
+const tmpdir = require('../common/tmpdir');
+
+tmpdir.refresh();
+
+let uniqueId = 0;
+
+function createParent() {
+  const dir = tmpdir.resolve(`dir-${uniqueId++}`);
+  fs.mkdirSync(dir);
+  const parent = new Module(path.join(dir, 'parent.js'));
+  parent.filename = parent.id;
+  parent.paths = Module._nodeModulePaths(dir);
+  let pathReads = 0;
+  Object.defineProperty(parent, 'path', {
+    __proto__: null,
+    get() {
+      pathReads++;
+      return dir;
+    },
+  });
+  return { dir, parent, getPathReads: () => pathReads };
+}
+
+// Builtins return before the relative resolve cache is populated, so the
+// parent directory should only be looked up, not also used to create a bucket.
+for (const request of ['node:path', 'path']) {
+  const { parent, getPathReads } = createParent();
+  assert.strictEqual(Module._load(request, parent, false), path);
+  assert.strictEqual(getPathReads(), 1);
+}
+
+// Same for resolutions that throw.
+{
+  const { parent, getPathReads } = createParent();
+  assert.throws(() => Module._load('./missing', parent, false),
+                { code: 'MODULE_NOT_FOUND' });
+  assert.strictEqual(getPathReads(), 1);
+}
+
+// A successful load populates the cache, and the second load hits it.
+{
+  const { dir, parent, getPathReads } = createParent();
+  fs.writeFileSync(path.join(dir, 'child.js'), 'module.exports = {};');
+  const first = Module._load('./child', parent, false);
+  assert.strictEqual(getPathReads(), 1);
+  assert.strictEqual(Module._load('./child', parent, false), first);
+  assert.strictEqual(getPathReads(), 2);
+}