module: cache nearest parent package.json per directory - #65326

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
codebytere:perf/module-parent-package-json-dir-cache
Aug 19, 2026
Merged

module: cache nearest parent package.json per directory#65326
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
codebytere:perf/module-parent-package-json-dir-cache

Conversation

@codebytere

@codebyterecodebytere commented Aug 16, 2026

Copy link
Copy Markdown
Member

Cold require() of a 1000-module CommonJS tree gets ~6 % faster (100 runs, ***) and module/module-require.js type='.js' +13.7 %, by looking up the nearest parent package.json once per directory instead of once per file.

module/module-require.js n=10000 type='.js' *** 13.69 % ±2.36%
module/module-require.js n=10000 type='.json' | 'dir' (don't use this path) -2.1 % (relink noise is ±1 %)
cold require() of a generated 1000-module tree, flat layout (100 runs) *** 6.37 % ±1.76%
same, pnpm-style symlinked node_modules *** 6.56 % ±0.90%
same, with NODE_COMPILE_CACHE warm *** 5.10 % ±1.43%

getNearestParentPackageJSON(checkPath) - called for every CommonJS module to find "type", and by findPackageJSON - memoized per file, so N modules in a directory meant N calls into the native package.json reader. The result only depends on the directory the traversal starts from, so the JS-side cache is now keyed by that directory, derived with the same rule the native side uses (trailing separator → that directory, else dirname). With the permission model enabled the per-path behavior is kept, because the native call performs a per-path read check. Same return values, same objects, same lifetime (both caches were already permanent). 1000 native calls → 236 for the tree above.

Tests: behavior is unchanged; test-module-*, test-require-*, test-esm-*, es-module and test-permission-* (which exercises the per-path branch) pass. Happy to add a call-count test if wanted.


Disclosure: the code, test, measurements and this description were written by Claude Code, directed and reviewed by @codebytere.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-botnodejs-github-bot added module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run. labels Aug 16, 2026
@codebyterecodebytere added request-ci Add this label to start a Jenkins CI on a PR. and removed needs-ci PRs that need a full CI run. labels Aug 16, 2026
@codecov

codecovBot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.95918% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.13%. Comparing base (30bff4a) to head (feee201).
⚠️ Report is 43 commits behind head on main.

Files with missing linesPatch %Lines
lib/internal/modules/package_json_reader.js97.95%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65326 +/- ##
=======================================
Coverage 90.13% 90.13% =======================================
Files 752 752 Lines 251568 251639 +71 Branches 47270 47285 +15 =======================================
+ Hits 226759 226827 +68 - Misses 16168 16174 +6 + Partials 8641 8638 -3 
Files with missing linesCoverage Δ
lib/internal/modules/package_json_reader.js99.23% <97.95%> (-0.22%)⬇️

... and 38 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 16, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@ljharb

Copy link
Copy Markdown
Member

This seems good, but surely there should be some tests that assert the caching behavior, to avoid regressions?

getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
@codebytere
codebytereforce-pushed the perf/module-parent-package-json-dir-cache branch from cbba4de to feee201CompareAugust 17, 2026 06:25
@codebytere

Copy link
Copy Markdown
MemberAuthor

@ljharb done

@codebyterecodebytere added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 18, 2026
@nodejs-github-botnodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 18, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator
Commit Queue failed
- Loading data for nodejs/node/pull/65326
✔ Done loading data for nodejs/node/pull/65326
----------------------------------- PR info ------------------------------------
Title module: cache nearest parent package.json per directory (#65326)
Author Shelley Vohr <shelley.vohr@gmail.com> (@codebytere)
Branch codebytere:perf/module-parent-package-json-dir-cache -> nodejs:main
Labels module, commit-queue
Commits 1
- module: cache nearest parent package.json per directory
Committers 1
- Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Sun, 16 Aug 2026 15:51:12 GMT
✔ Approvals: 3
✔ - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/65326#pullrequestreview-4947040173
✔ - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/65326#pullrequestreview-4948264808
✔ - Jordan Harband (@ljharb): https://github.com/nodejs/node/pull/65326#pullrequestreview-4953177652
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-16T18:43:54Z: https://ci.nodejs.org/job/node-test-pull-request/75894/
⚠ Commits were pushed after the last Full PR CI run:
⚠ - module: cache nearest parent package.json per directory
- Querying data for job/node-test-pull-request/75894/
✔ Build data downloaded
- Querying failures of job/node-test-commit/90623/
✔ Data downloaded
✘ 2 failure(s) on the last Jenkins CI run
--------------------------------------------------------------------------------
✔ Aborted `git node land` session in /home/runner/work/node/node/.ncu
https://github.com/nodejs/node/actions/runs/32157693157

@codebyterecodebytere added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 18, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 18, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@codebyterecodebytere added commit-queue PRs queued for automated landing through the Commit Queue. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Aug 19, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 9c641c5 into nodejs:mainAug 19, 2026
78 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 9c641c5

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 19, 2026
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

moduleIssues and PRs related to the module subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@codebytere@nodejs-github-bot@ljharb@jasnell@anonrig@jsumners-nr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

module: cache nearest parent package.json per directory - #65326

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
codebytere:perf/module-parent-package-json-dir-cache
Aug 19, 2026
Merged

module: cache nearest parent package.json per directory#65326
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
codebytere:perf/module-parent-package-json-dir-cache

Conversation

@codebytere

@codebyterecodebytere commented Aug 16, 2026

Copy link
Copy Markdown
Member

Cold require() of a 1000-module CommonJS tree gets ~6 % faster (100 runs, ***) and module/module-require.js type='.js' +13.7 %, by looking up the nearest parent package.json once per directory instead of once per file.

module/module-require.js n=10000 type='.js' *** 13.69 % ±2.36%
module/module-require.js n=10000 type='.json' | 'dir' (don't use this path) -2.1 % (relink noise is ±1 %)
cold require() of a generated 1000-module tree, flat layout (100 runs) *** 6.37 % ±1.76%
same, pnpm-style symlinked node_modules *** 6.56 % ±0.90%
same, with NODE_COMPILE_CACHE warm *** 5.10 % ±1.43%

getNearestParentPackageJSON(checkPath) - called for every CommonJS module to find "type", and by findPackageJSON - memoized per file, so N modules in a directory meant N calls into the native package.json reader. The result only depends on the directory the traversal starts from, so the JS-side cache is now keyed by that directory, derived with the same rule the native side uses (trailing separator → that directory, else dirname). With the permission model enabled the per-path behavior is kept, because the native call performs a per-path read check. Same return values, same objects, same lifetime (both caches were already permanent). 1000 native calls → 236 for the tree above.

Tests: behavior is unchanged; test-module-*, test-require-*, test-esm-*, es-module and test-permission-* (which exercises the per-path branch) pass. Happy to add a call-count test if wanted.


Disclosure: the code, test, measurements and this description were written by Claude Code, directed and reviewed by @codebytere.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-botnodejs-github-bot added module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run. labels Aug 16, 2026
@codebyterecodebytere added request-ci Add this label to start a Jenkins CI on a PR. and removed needs-ci PRs that need a full CI run. labels Aug 16, 2026
@codecov

codecovBot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.95918% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.13%. Comparing base (30bff4a) to head (feee201).
⚠️ Report is 43 commits behind head on main.

Files with missing linesPatch %Lines
lib/internal/modules/package_json_reader.js97.95%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65326 +/- ##
=======================================
Coverage 90.13% 90.13% =======================================
Files 752 752 Lines 251568 251639 +71 Branches 47270 47285 +15 =======================================
+ Hits 226759 226827 +68 - Misses 16168 16174 +6 + Partials 8641 8638 -3 
Files with missing linesCoverage Δ
lib/internal/modules/package_json_reader.js99.23% <97.95%> (-0.22%)⬇️

... and 38 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 16, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@ljharb

Copy link
Copy Markdown
Member

This seems good, but surely there should be some tests that assert the caching behavior, to avoid regressions?

getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
@codebytere
codebytereforce-pushed the perf/module-parent-package-json-dir-cache branch from cbba4de to feee201CompareAugust 17, 2026 06:25
@codebytere

Copy link
Copy Markdown
MemberAuthor

@ljharb done

@codebyterecodebytere added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 18, 2026
@nodejs-github-botnodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 18, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator
Commit Queue failed
- Loading data for nodejs/node/pull/65326
✔ Done loading data for nodejs/node/pull/65326
----------------------------------- PR info ------------------------------------
Title module: cache nearest parent package.json per directory (#65326)
Author Shelley Vohr <shelley.vohr@gmail.com> (@codebytere)
Branch codebytere:perf/module-parent-package-json-dir-cache -> nodejs:main
Labels module, commit-queue
Commits 1
- module: cache nearest parent package.json per directory
Committers 1
- Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Sun, 16 Aug 2026 15:51:12 GMT
✔ Approvals: 3
✔ - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/65326#pullrequestreview-4947040173
✔ - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/65326#pullrequestreview-4948264808
✔ - Jordan Harband (@ljharb): https://github.com/nodejs/node/pull/65326#pullrequestreview-4953177652
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-16T18:43:54Z: https://ci.nodejs.org/job/node-test-pull-request/75894/
⚠ Commits were pushed after the last Full PR CI run:
⚠ - module: cache nearest parent package.json per directory
- Querying data for job/node-test-pull-request/75894/
✔ Build data downloaded
- Querying failures of job/node-test-commit/90623/
✔ Data downloaded
✘ 2 failure(s) on the last Jenkins CI run
--------------------------------------------------------------------------------
✔ Aborted `git node land` session in /home/runner/work/node/node/.ncu
https://github.com/nodejs/node/actions/runs/32157693157

@codebyterecodebytere added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 18, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 18, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@codebyterecodebytere added commit-queue PRs queued for automated landing through the Commit Queue. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Aug 19, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 9c641c5 into nodejs:mainAug 19, 2026
78 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 9c641c5

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 19, 2026
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

moduleIssues and PRs related to the module subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@codebytere@nodejs-github-bot@ljharb@jasnell@anonrig@jsumners-nr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

module: cache nearest parent package.json per directory - #65326

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
codebytere:perf/module-parent-package-json-dir-cache
Aug 19, 2026
Merged

module: cache nearest parent package.json per directory#65326
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
codebytere:perf/module-parent-package-json-dir-cache

Conversation

@codebytere

@codebyterecodebytere commented Aug 16, 2026

Copy link
Copy Markdown
Member

Cold require() of a 1000-module CommonJS tree gets ~6 % faster (100 runs, ***) and module/module-require.js type='.js' +13.7 %, by looking up the nearest parent package.json once per directory instead of once per file.

module/module-require.js n=10000 type='.js' *** 13.69 % ±2.36%
module/module-require.js n=10000 type='.json' | 'dir' (don't use this path) -2.1 % (relink noise is ±1 %)
cold require() of a generated 1000-module tree, flat layout (100 runs) *** 6.37 % ±1.76%
same, pnpm-style symlinked node_modules *** 6.56 % ±0.90%
same, with NODE_COMPILE_CACHE warm *** 5.10 % ±1.43%

getNearestParentPackageJSON(checkPath) - called for every CommonJS module to find "type", and by findPackageJSON - memoized per file, so N modules in a directory meant N calls into the native package.json reader. The result only depends on the directory the traversal starts from, so the JS-side cache is now keyed by that directory, derived with the same rule the native side uses (trailing separator → that directory, else dirname). With the permission model enabled the per-path behavior is kept, because the native call performs a per-path read check. Same return values, same objects, same lifetime (both caches were already permanent). 1000 native calls → 236 for the tree above.

Tests: behavior is unchanged; test-module-*, test-require-*, test-esm-*, es-module and test-permission-* (which exercises the per-path branch) pass. Happy to add a call-count test if wanted.


Disclosure: the code, test, measurements and this description were written by Claude Code, directed and reviewed by @codebytere.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-botnodejs-github-bot added module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run. labels Aug 16, 2026
@codebyterecodebytere added request-ci Add this label to start a Jenkins CI on a PR. and removed needs-ci PRs that need a full CI run. labels Aug 16, 2026
@codecov

codecovBot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.95918% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.13%. Comparing base (30bff4a) to head (feee201).
⚠️ Report is 43 commits behind head on main.

Files with missing linesPatch %Lines
lib/internal/modules/package_json_reader.js97.95%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65326 +/- ##
=======================================
Coverage 90.13% 90.13% =======================================
Files 752 752 Lines 251568 251639 +71 Branches 47270 47285 +15 =======================================
+ Hits 226759 226827 +68 - Misses 16168 16174 +6 + Partials 8641 8638 -3 
Files with missing linesCoverage Δ
lib/internal/modules/package_json_reader.js99.23% <97.95%> (-0.22%)⬇️

... and 38 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 16, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@ljharb

Copy link
Copy Markdown
Member

This seems good, but surely there should be some tests that assert the caching behavior, to avoid regressions?

getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
@codebytere
codebytereforce-pushed the perf/module-parent-package-json-dir-cache branch from cbba4de to feee201CompareAugust 17, 2026 06:25
@codebytere

Copy link
Copy Markdown
MemberAuthor

@ljharb done

@codebyterecodebytere added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 18, 2026
@nodejs-github-botnodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 18, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator
Commit Queue failed
- Loading data for nodejs/node/pull/65326
✔ Done loading data for nodejs/node/pull/65326
----------------------------------- PR info ------------------------------------
Title module: cache nearest parent package.json per directory (#65326)
Author Shelley Vohr <shelley.vohr@gmail.com> (@codebytere)
Branch codebytere:perf/module-parent-package-json-dir-cache -> nodejs:main
Labels module, commit-queue
Commits 1
- module: cache nearest parent package.json per directory
Committers 1
- Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Sun, 16 Aug 2026 15:51:12 GMT
✔ Approvals: 3
✔ - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/65326#pullrequestreview-4947040173
✔ - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/65326#pullrequestreview-4948264808
✔ - Jordan Harband (@ljharb): https://github.com/nodejs/node/pull/65326#pullrequestreview-4953177652
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-16T18:43:54Z: https://ci.nodejs.org/job/node-test-pull-request/75894/
⚠ Commits were pushed after the last Full PR CI run:
⚠ - module: cache nearest parent package.json per directory
- Querying data for job/node-test-pull-request/75894/
✔ Build data downloaded
- Querying failures of job/node-test-commit/90623/
✔ Data downloaded
✘ 2 failure(s) on the last Jenkins CI run
--------------------------------------------------------------------------------
✔ Aborted `git node land` session in /home/runner/work/node/node/.ncu
https://github.com/nodejs/node/actions/runs/32157693157

@codebyterecodebytere added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 18, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 18, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@codebyterecodebytere added commit-queue PRs queued for automated landing through the Commit Queue. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Aug 19, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 9c641c5 into nodejs:mainAug 19, 2026
78 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 9c641c5

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 19, 2026
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

moduleIssues and PRs related to the module subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@codebytere@nodejs-github-bot@ljharb@jasnell@anonrig@jsumners-nr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

module: cache nearest parent package.json per directory - #65326

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
codebytere:perf/module-parent-package-json-dir-cache
Aug 19, 2026
Merged

module: cache nearest parent package.json per directory#65326
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
codebytere:perf/module-parent-package-json-dir-cache

Conversation

@codebytere

@codebyterecodebytere commented Aug 16, 2026

Copy link
Copy Markdown
Member

Cold require() of a 1000-module CommonJS tree gets ~6 % faster (100 runs, ***) and module/module-require.js type='.js' +13.7 %, by looking up the nearest parent package.json once per directory instead of once per file.

module/module-require.js n=10000 type='.js' *** 13.69 % ±2.36%
module/module-require.js n=10000 type='.json' | 'dir' (don't use this path) -2.1 % (relink noise is ±1 %)
cold require() of a generated 1000-module tree, flat layout (100 runs) *** 6.37 % ±1.76%
same, pnpm-style symlinked node_modules *** 6.56 % ±0.90%
same, with NODE_COMPILE_CACHE warm *** 5.10 % ±1.43%

getNearestParentPackageJSON(checkPath) - called for every CommonJS module to find "type", and by findPackageJSON - memoized per file, so N modules in a directory meant N calls into the native package.json reader. The result only depends on the directory the traversal starts from, so the JS-side cache is now keyed by that directory, derived with the same rule the native side uses (trailing separator → that directory, else dirname). With the permission model enabled the per-path behavior is kept, because the native call performs a per-path read check. Same return values, same objects, same lifetime (both caches were already permanent). 1000 native calls → 236 for the tree above.

Tests: behavior is unchanged; test-module-*, test-require-*, test-esm-*, es-module and test-permission-* (which exercises the per-path branch) pass. Happy to add a call-count test if wanted.


Disclosure: the code, test, measurements and this description were written by Claude Code, directed and reviewed by @codebytere.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-botnodejs-github-bot added module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run. labels Aug 16, 2026
@codebyterecodebytere added request-ci Add this label to start a Jenkins CI on a PR. and removed needs-ci PRs that need a full CI run. labels Aug 16, 2026
@codecov

codecovBot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.95918% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.13%. Comparing base (30bff4a) to head (feee201).
⚠️ Report is 43 commits behind head on main.

Files with missing linesPatch %Lines
lib/internal/modules/package_json_reader.js97.95%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65326 +/- ##
=======================================
Coverage 90.13% 90.13% =======================================
Files 752 752 Lines 251568 251639 +71 Branches 47270 47285 +15 =======================================
+ Hits 226759 226827 +68 - Misses 16168 16174 +6 + Partials 8641 8638 -3 
Files with missing linesCoverage Δ
lib/internal/modules/package_json_reader.js99.23% <97.95%> (-0.22%)⬇️

... and 38 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 16, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@ljharb

Copy link
Copy Markdown
Member

This seems good, but surely there should be some tests that assert the caching behavior, to avoid regressions?

getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
@codebytere
codebytereforce-pushed the perf/module-parent-package-json-dir-cache branch from cbba4de to feee201CompareAugust 17, 2026 06:25
@codebytere

Copy link
Copy Markdown
MemberAuthor

@ljharb done

@codebyterecodebytere added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 18, 2026
@nodejs-github-botnodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 18, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator
Commit Queue failed
- Loading data for nodejs/node/pull/65326
✔ Done loading data for nodejs/node/pull/65326
----------------------------------- PR info ------------------------------------
Title module: cache nearest parent package.json per directory (#65326)
Author Shelley Vohr <shelley.vohr@gmail.com> (@codebytere)
Branch codebytere:perf/module-parent-package-json-dir-cache -> nodejs:main
Labels module, commit-queue
Commits 1
- module: cache nearest parent package.json per directory
Committers 1
- Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Sun, 16 Aug 2026 15:51:12 GMT
✔ Approvals: 3
✔ - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/65326#pullrequestreview-4947040173
✔ - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/65326#pullrequestreview-4948264808
✔ - Jordan Harband (@ljharb): https://github.com/nodejs/node/pull/65326#pullrequestreview-4953177652
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-16T18:43:54Z: https://ci.nodejs.org/job/node-test-pull-request/75894/
⚠ Commits were pushed after the last Full PR CI run:
⚠ - module: cache nearest parent package.json per directory
- Querying data for job/node-test-pull-request/75894/
✔ Build data downloaded
- Querying failures of job/node-test-commit/90623/
✔ Data downloaded
✘ 2 failure(s) on the last Jenkins CI run
--------------------------------------------------------------------------------
✔ Aborted `git node land` session in /home/runner/work/node/node/.ncu
https://github.com/nodejs/node/actions/runs/32157693157

@codebyterecodebytere added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 18, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 18, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@codebyterecodebytere added commit-queue PRs queued for automated landing through the Commit Queue. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Aug 19, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 9c641c5 into nodejs:mainAug 19, 2026
78 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 9c641c5

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 19, 2026
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

moduleIssues and PRs related to the module subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@codebytere@nodejs-github-bot@ljharb@jasnell@anonrig@jsumners-nr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

module: cache nearest parent package.json per directory - #65326

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
codebytere:perf/module-parent-package-json-dir-cache
Aug 19, 2026
Merged

module: cache nearest parent package.json per directory#65326
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
codebytere:perf/module-parent-package-json-dir-cache

Conversation

@codebytere

@codebyterecodebytere commented Aug 16, 2026

Copy link
Copy Markdown
Member

Cold require() of a 1000-module CommonJS tree gets ~6 % faster (100 runs, ***) and module/module-require.js type='.js' +13.7 %, by looking up the nearest parent package.json once per directory instead of once per file.

module/module-require.js n=10000 type='.js' *** 13.69 % ±2.36%
module/module-require.js n=10000 type='.json' | 'dir' (don't use this path) -2.1 % (relink noise is ±1 %)
cold require() of a generated 1000-module tree, flat layout (100 runs) *** 6.37 % ±1.76%
same, pnpm-style symlinked node_modules *** 6.56 % ±0.90%
same, with NODE_COMPILE_CACHE warm *** 5.10 % ±1.43%

getNearestParentPackageJSON(checkPath) - called for every CommonJS module to find "type", and by findPackageJSON - memoized per file, so N modules in a directory meant N calls into the native package.json reader. The result only depends on the directory the traversal starts from, so the JS-side cache is now keyed by that directory, derived with the same rule the native side uses (trailing separator → that directory, else dirname). With the permission model enabled the per-path behavior is kept, because the native call performs a per-path read check. Same return values, same objects, same lifetime (both caches were already permanent). 1000 native calls → 236 for the tree above.

Tests: behavior is unchanged; test-module-*, test-require-*, test-esm-*, es-module and test-permission-* (which exercises the per-path branch) pass. Happy to add a call-count test if wanted.


Disclosure: the code, test, measurements and this description were written by Claude Code, directed and reviewed by @codebytere.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-botnodejs-github-bot added module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run. labels Aug 16, 2026
@codebyterecodebytere added request-ci Add this label to start a Jenkins CI on a PR. and removed needs-ci PRs that need a full CI run. labels Aug 16, 2026
@codecov

codecovBot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.95918% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.13%. Comparing base (30bff4a) to head (feee201).
⚠️ Report is 43 commits behind head on main.

Files with missing linesPatch %Lines
lib/internal/modules/package_json_reader.js97.95%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65326 +/- ##
=======================================
Coverage 90.13% 90.13% =======================================
Files 752 752 Lines 251568 251639 +71 Branches 47270 47285 +15 =======================================
+ Hits 226759 226827 +68 - Misses 16168 16174 +6 + Partials 8641 8638 -3 
Files with missing linesCoverage Δ
lib/internal/modules/package_json_reader.js99.23% <97.95%> (-0.22%)⬇️

... and 38 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 16, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@ljharb

Copy link
Copy Markdown
Member

This seems good, but surely there should be some tests that assert the caching behavior, to avoid regressions?

getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
@codebytere
codebytereforce-pushed the perf/module-parent-package-json-dir-cache branch from cbba4de to feee201CompareAugust 17, 2026 06:25
@codebytere

Copy link
Copy Markdown
MemberAuthor

@ljharb done

@codebyterecodebytere added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 18, 2026
@nodejs-github-botnodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 18, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator
Commit Queue failed
- Loading data for nodejs/node/pull/65326
✔ Done loading data for nodejs/node/pull/65326
----------------------------------- PR info ------------------------------------
Title module: cache nearest parent package.json per directory (#65326)
Author Shelley Vohr <shelley.vohr@gmail.com> (@codebytere)
Branch codebytere:perf/module-parent-package-json-dir-cache -> nodejs:main
Labels module, commit-queue
Commits 1
- module: cache nearest parent package.json per directory
Committers 1
- Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Sun, 16 Aug 2026 15:51:12 GMT
✔ Approvals: 3
✔ - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/65326#pullrequestreview-4947040173
✔ - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/65326#pullrequestreview-4948264808
✔ - Jordan Harband (@ljharb): https://github.com/nodejs/node/pull/65326#pullrequestreview-4953177652
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-16T18:43:54Z: https://ci.nodejs.org/job/node-test-pull-request/75894/
⚠ Commits were pushed after the last Full PR CI run:
⚠ - module: cache nearest parent package.json per directory
- Querying data for job/node-test-pull-request/75894/
✔ Build data downloaded
- Querying failures of job/node-test-commit/90623/
✔ Data downloaded
✘ 2 failure(s) on the last Jenkins CI run
--------------------------------------------------------------------------------
✔ Aborted `git node land` session in /home/runner/work/node/node/.ncu
https://github.com/nodejs/node/actions/runs/32157693157

@codebyterecodebytere added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 18, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 18, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@codebyterecodebytere added commit-queue PRs queued for automated landing through the Commit Queue. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Aug 19, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 9c641c5 into nodejs:mainAug 19, 2026
78 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 9c641c5

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 19, 2026
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

moduleIssues and PRs related to the module subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@codebytere@nodejs-github-bot@ljharb@jasnell@anonrig@jsumners-nr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

module: cache nearest parent package.json per directory - #65326

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
codebytere:perf/module-parent-package-json-dir-cache
Aug 19, 2026
Merged

module: cache nearest parent package.json per directory#65326
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
codebytere:perf/module-parent-package-json-dir-cache

Conversation

@codebytere

@codebyterecodebytere commented Aug 16, 2026

Copy link
Copy Markdown
Member

Cold require() of a 1000-module CommonJS tree gets ~6 % faster (100 runs, ***) and module/module-require.js type='.js' +13.7 %, by looking up the nearest parent package.json once per directory instead of once per file.

module/module-require.js n=10000 type='.js' *** 13.69 % ±2.36%
module/module-require.js n=10000 type='.json' | 'dir' (don't use this path) -2.1 % (relink noise is ±1 %)
cold require() of a generated 1000-module tree, flat layout (100 runs) *** 6.37 % ±1.76%
same, pnpm-style symlinked node_modules *** 6.56 % ±0.90%
same, with NODE_COMPILE_CACHE warm *** 5.10 % ±1.43%

getNearestParentPackageJSON(checkPath) - called for every CommonJS module to find "type", and by findPackageJSON - memoized per file, so N modules in a directory meant N calls into the native package.json reader. The result only depends on the directory the traversal starts from, so the JS-side cache is now keyed by that directory, derived with the same rule the native side uses (trailing separator → that directory, else dirname). With the permission model enabled the per-path behavior is kept, because the native call performs a per-path read check. Same return values, same objects, same lifetime (both caches were already permanent). 1000 native calls → 236 for the tree above.

Tests: behavior is unchanged; test-module-*, test-require-*, test-esm-*, es-module and test-permission-* (which exercises the per-path branch) pass. Happy to add a call-count test if wanted.


Disclosure: the code, test, measurements and this description were written by Claude Code, directed and reviewed by @codebytere.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-botnodejs-github-bot added module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run. labels Aug 16, 2026
@codebyterecodebytere added request-ci Add this label to start a Jenkins CI on a PR. and removed needs-ci PRs that need a full CI run. labels Aug 16, 2026
@codecov

codecovBot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.95918% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.13%. Comparing base (30bff4a) to head (feee201).
⚠️ Report is 43 commits behind head on main.

Files with missing linesPatch %Lines
lib/internal/modules/package_json_reader.js97.95%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65326 +/- ##
=======================================
Coverage 90.13% 90.13% =======================================
Files 752 752 Lines 251568 251639 +71 Branches 47270 47285 +15 =======================================
+ Hits 226759 226827 +68 - Misses 16168 16174 +6 + Partials 8641 8638 -3 
Files with missing linesCoverage Δ
lib/internal/modules/package_json_reader.js99.23% <97.95%> (-0.22%)⬇️

... and 38 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 16, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@ljharb

Copy link
Copy Markdown
Member

This seems good, but surely there should be some tests that assert the caching behavior, to avoid regressions?

getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
@codebytere
codebytereforce-pushed the perf/module-parent-package-json-dir-cache branch from cbba4de to feee201CompareAugust 17, 2026 06:25
@codebytere

Copy link
Copy Markdown
MemberAuthor

@ljharb done

@codebyterecodebytere added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 18, 2026
@nodejs-github-botnodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 18, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator
Commit Queue failed
- Loading data for nodejs/node/pull/65326
✔ Done loading data for nodejs/node/pull/65326
----------------------------------- PR info ------------------------------------
Title module: cache nearest parent package.json per directory (#65326)
Author Shelley Vohr <shelley.vohr@gmail.com> (@codebytere)
Branch codebytere:perf/module-parent-package-json-dir-cache -> nodejs:main
Labels module, commit-queue
Commits 1
- module: cache nearest parent package.json per directory
Committers 1
- Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Sun, 16 Aug 2026 15:51:12 GMT
✔ Approvals: 3
✔ - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/65326#pullrequestreview-4947040173
✔ - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/65326#pullrequestreview-4948264808
✔ - Jordan Harband (@ljharb): https://github.com/nodejs/node/pull/65326#pullrequestreview-4953177652
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-16T18:43:54Z: https://ci.nodejs.org/job/node-test-pull-request/75894/
⚠ Commits were pushed after the last Full PR CI run:
⚠ - module: cache nearest parent package.json per directory
- Querying data for job/node-test-pull-request/75894/
✔ Build data downloaded
- Querying failures of job/node-test-commit/90623/
✔ Data downloaded
✘ 2 failure(s) on the last Jenkins CI run
--------------------------------------------------------------------------------
✔ Aborted `git node land` session in /home/runner/work/node/node/.ncu
https://github.com/nodejs/node/actions/runs/32157693157

@codebyterecodebytere added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 18, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 18, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@codebyterecodebytere added commit-queue PRs queued for automated landing through the Commit Queue. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Aug 19, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 9c641c5 into nodejs:mainAug 19, 2026
78 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 9c641c5

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 19, 2026
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

moduleIssues and PRs related to the module subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@codebytere@nodejs-github-bot@ljharb@jasnell@anonrig@jsumners-nr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

module: cache nearest parent package.json per directory - #65326

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
codebytere:perf/module-parent-package-json-dir-cache
Aug 19, 2026
Merged

module: cache nearest parent package.json per directory#65326
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
codebytere:perf/module-parent-package-json-dir-cache

Conversation

@codebytere

@codebyterecodebytere commented Aug 16, 2026

Copy link
Copy Markdown
Member

Cold require() of a 1000-module CommonJS tree gets ~6 % faster (100 runs, ***) and module/module-require.js type='.js' +13.7 %, by looking up the nearest parent package.json once per directory instead of once per file.

module/module-require.js n=10000 type='.js' *** 13.69 % ±2.36%
module/module-require.js n=10000 type='.json' | 'dir' (don't use this path) -2.1 % (relink noise is ±1 %)
cold require() of a generated 1000-module tree, flat layout (100 runs) *** 6.37 % ±1.76%
same, pnpm-style symlinked node_modules *** 6.56 % ±0.90%
same, with NODE_COMPILE_CACHE warm *** 5.10 % ±1.43%

getNearestParentPackageJSON(checkPath) - called for every CommonJS module to find "type", and by findPackageJSON - memoized per file, so N modules in a directory meant N calls into the native package.json reader. The result only depends on the directory the traversal starts from, so the JS-side cache is now keyed by that directory, derived with the same rule the native side uses (trailing separator → that directory, else dirname). With the permission model enabled the per-path behavior is kept, because the native call performs a per-path read check. Same return values, same objects, same lifetime (both caches were already permanent). 1000 native calls → 236 for the tree above.

Tests: behavior is unchanged; test-module-*, test-require-*, test-esm-*, es-module and test-permission-* (which exercises the per-path branch) pass. Happy to add a call-count test if wanted.


Disclosure: the code, test, measurements and this description were written by Claude Code, directed and reviewed by @codebytere.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-botnodejs-github-bot added module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run. labels Aug 16, 2026
@codebyterecodebytere added request-ci Add this label to start a Jenkins CI on a PR. and removed needs-ci PRs that need a full CI run. labels Aug 16, 2026
@codecov

codecovBot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.95918% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.13%. Comparing base (30bff4a) to head (feee201).
⚠️ Report is 43 commits behind head on main.

Files with missing linesPatch %Lines
lib/internal/modules/package_json_reader.js97.95%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65326 +/- ##
=======================================
Coverage 90.13% 90.13% =======================================
Files 752 752 Lines 251568 251639 +71 Branches 47270 47285 +15 =======================================
+ Hits 226759 226827 +68 - Misses 16168 16174 +6 + Partials 8641 8638 -3 
Files with missing linesCoverage Δ
lib/internal/modules/package_json_reader.js99.23% <97.95%> (-0.22%)⬇️

... and 38 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 16, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@ljharb

Copy link
Copy Markdown
Member

This seems good, but surely there should be some tests that assert the caching behavior, to avoid regressions?

getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
@codebytere
codebytereforce-pushed the perf/module-parent-package-json-dir-cache branch from cbba4de to feee201CompareAugust 17, 2026 06:25
@codebytere

Copy link
Copy Markdown
MemberAuthor

@ljharb done

@codebyterecodebytere added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 18, 2026
@nodejs-github-botnodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 18, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator
Commit Queue failed
- Loading data for nodejs/node/pull/65326
✔ Done loading data for nodejs/node/pull/65326
----------------------------------- PR info ------------------------------------
Title module: cache nearest parent package.json per directory (#65326)
Author Shelley Vohr <shelley.vohr@gmail.com> (@codebytere)
Branch codebytere:perf/module-parent-package-json-dir-cache -> nodejs:main
Labels module, commit-queue
Commits 1
- module: cache nearest parent package.json per directory
Committers 1
- Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Sun, 16 Aug 2026 15:51:12 GMT
✔ Approvals: 3
✔ - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/65326#pullrequestreview-4947040173
✔ - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/65326#pullrequestreview-4948264808
✔ - Jordan Harband (@ljharb): https://github.com/nodejs/node/pull/65326#pullrequestreview-4953177652
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-16T18:43:54Z: https://ci.nodejs.org/job/node-test-pull-request/75894/
⚠ Commits were pushed after the last Full PR CI run:
⚠ - module: cache nearest parent package.json per directory
- Querying data for job/node-test-pull-request/75894/
✔ Build data downloaded
- Querying failures of job/node-test-commit/90623/
✔ Data downloaded
✘ 2 failure(s) on the last Jenkins CI run
--------------------------------------------------------------------------------
✔ Aborted `git node land` session in /home/runner/work/node/node/.ncu
https://github.com/nodejs/node/actions/runs/32157693157

@codebyterecodebytere added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 18, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 18, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@codebyterecodebytere added commit-queue PRs queued for automated landing through the Commit Queue. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Aug 19, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 9c641c5 into nodejs:mainAug 19, 2026
78 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 9c641c5

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 19, 2026
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

moduleIssues and PRs related to the module subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@codebytere@nodejs-github-bot@ljharb@jasnell@anonrig@jsumners-nr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

module: cache nearest parent package.json per directory - #65326

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
codebytere:perf/module-parent-package-json-dir-cache
Aug 19, 2026
Merged

module: cache nearest parent package.json per directory#65326
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
codebytere:perf/module-parent-package-json-dir-cache

Conversation

@codebytere

@codebyterecodebytere commented Aug 16, 2026

Copy link
Copy Markdown
Member

Cold require() of a 1000-module CommonJS tree gets ~6 % faster (100 runs, ***) and module/module-require.js type='.js' +13.7 %, by looking up the nearest parent package.json once per directory instead of once per file.

module/module-require.js n=10000 type='.js' *** 13.69 % ±2.36%
module/module-require.js n=10000 type='.json' | 'dir' (don't use this path) -2.1 % (relink noise is ±1 %)
cold require() of a generated 1000-module tree, flat layout (100 runs) *** 6.37 % ±1.76%
same, pnpm-style symlinked node_modules *** 6.56 % ±0.90%
same, with NODE_COMPILE_CACHE warm *** 5.10 % ±1.43%

getNearestParentPackageJSON(checkPath) - called for every CommonJS module to find "type", and by findPackageJSON - memoized per file, so N modules in a directory meant N calls into the native package.json reader. The result only depends on the directory the traversal starts from, so the JS-side cache is now keyed by that directory, derived with the same rule the native side uses (trailing separator → that directory, else dirname). With the permission model enabled the per-path behavior is kept, because the native call performs a per-path read check. Same return values, same objects, same lifetime (both caches were already permanent). 1000 native calls → 236 for the tree above.

Tests: behavior is unchanged; test-module-*, test-require-*, test-esm-*, es-module and test-permission-* (which exercises the per-path branch) pass. Happy to add a call-count test if wanted.


Disclosure: the code, test, measurements and this description were written by Claude Code, directed and reviewed by @codebytere.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-botnodejs-github-bot added module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run. labels Aug 16, 2026
@codebyterecodebytere added request-ci Add this label to start a Jenkins CI on a PR. and removed needs-ci PRs that need a full CI run. labels Aug 16, 2026
@codecov

codecovBot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.95918% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.13%. Comparing base (30bff4a) to head (feee201).
⚠️ Report is 43 commits behind head on main.

Files with missing linesPatch %Lines
lib/internal/modules/package_json_reader.js97.95%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65326 +/- ##
=======================================
Coverage 90.13% 90.13% =======================================
Files 752 752 Lines 251568 251639 +71 Branches 47270 47285 +15 =======================================
+ Hits 226759 226827 +68 - Misses 16168 16174 +6 + Partials 8641 8638 -3 
Files with missing linesCoverage Δ
lib/internal/modules/package_json_reader.js99.23% <97.95%> (-0.22%)⬇️

... and 38 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 16, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@ljharb

Copy link
Copy Markdown
Member

This seems good, but surely there should be some tests that assert the caching behavior, to avoid regressions?

getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
@codebytere
codebytereforce-pushed the perf/module-parent-package-json-dir-cache branch from cbba4de to feee201CompareAugust 17, 2026 06:25
@codebytere

Copy link
Copy Markdown
MemberAuthor

@ljharb done

@codebyterecodebytere added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 18, 2026
@nodejs-github-botnodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 18, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator
Commit Queue failed
- Loading data for nodejs/node/pull/65326
✔ Done loading data for nodejs/node/pull/65326
----------------------------------- PR info ------------------------------------
Title module: cache nearest parent package.json per directory (#65326)
Author Shelley Vohr <shelley.vohr@gmail.com> (@codebytere)
Branch codebytere:perf/module-parent-package-json-dir-cache -> nodejs:main
Labels module, commit-queue
Commits 1
- module: cache nearest parent package.json per directory
Committers 1
- Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Sun, 16 Aug 2026 15:51:12 GMT
✔ Approvals: 3
✔ - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/65326#pullrequestreview-4947040173
✔ - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/65326#pullrequestreview-4948264808
✔ - Jordan Harband (@ljharb): https://github.com/nodejs/node/pull/65326#pullrequestreview-4953177652
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-16T18:43:54Z: https://ci.nodejs.org/job/node-test-pull-request/75894/
⚠ Commits were pushed after the last Full PR CI run:
⚠ - module: cache nearest parent package.json per directory
- Querying data for job/node-test-pull-request/75894/
✔ Build data downloaded
- Querying failures of job/node-test-commit/90623/
✔ Data downloaded
✘ 2 failure(s) on the last Jenkins CI run
--------------------------------------------------------------------------------
✔ Aborted `git node land` session in /home/runner/work/node/node/.ncu
https://github.com/nodejs/node/actions/runs/32157693157

@codebyterecodebytere added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 18, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 18, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@codebyterecodebytere added commit-queue PRs queued for automated landing through the Commit Queue. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Aug 19, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 9c641c5 into nodejs:mainAug 19, 2026
78 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 9c641c5

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 19, 2026
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.
The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.
Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: #65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

moduleIssues and PRs related to the module subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@codebytere@nodejs-github-bot@ljharb@jasnell@anonrig@jsumners-nr