Skip to content

module: synchronously load most ES modules#62530

Open
GeoffreyBooth wants to merge 1 commit intonodejs:mainfrom
GeoffreyBooth:synchronously-load-most-es-modules
Open

module: synchronously load most ES modules#62530
GeoffreyBooth wants to merge 1 commit intonodejs:mainfrom
GeoffreyBooth:synchronously-load-most-es-modules

Conversation

@GeoffreyBooth
Copy link
Copy Markdown
Member

Building on #55782, this PR uses the path @joyeecheung created for require(esm) to synchronously resolve and load all ES modules that lack top-level await, which is the vast majority of modules. The sync path is used when no async loader hooks, --import flags, or --inspect-brk are active; it falls back to the existing async path otherwise. Top-level await presence can only be determined after the module graph is instantiated, so if TLA is detected the already-instantiated graph falls back to async evaluation. In all cases the behavior is identical to the existing async path.

On current main, an ES module graph generates 14 + 5N promises for N modules; so 19 promises for a single module graph (one entry point that doesn’t import anything), 24 promises if that entry point imports one file, 29 promises for a three-module graph and so on.

In this PR, only one promise is created regardless of graph size: the low-level V8 module.evaluate() call that happens within module.evaluateSync(), where an immediately-resolved promise is created even for modules that don’t have top-level await. But still, it’s only one promise for an entire application, no matter how big the app is.

This PR adds a benchmark that focuses on the module loading flow that this PR improves:

                                           confidence improvement accuracy (*)   (**)  (***)
esm/startup-esm-graph.js n=30 modules=250         ***    -29.85 %       ±2.96% ±3.90% ±5.00%
esm/startup-esm-graph.js n=30 modules=500         ***    -22.17 %       ±3.07% ±4.04% ±5.18%
esm/startup-esm-graph.js n=30 modules=1000        ***    -16.21 %       ±3.19% ±4.20% ±5.38%
esm/startup-esm-graph.js n=30 modules=2000        ***     -9.46 %       ±3.33% ±4.39% ±5.62%

Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 4 comparisons, you can thus
expect the following amount of false-positive results:
  0.20 false positives, when considering a   5% risk acceptance (*, **, ***),
  0.04 false positives, when considering a   1% risk acceptance (**, ***),
  0.00 false positives, when considering a 0.1% risk acceptance (***)

The improvement is roughly halving for each doubling of the number of modules, consistent with I/O becoming the dominant cost as the size of the graph grows.

@nodejs-github-bot
Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/loaders
  • @nodejs/performance

@nodejs-github-bot nodejs-github-bot added esm Issues and PRs related to the ECMAScript Modules implementation. module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run. labels Apr 1, 2026
@GeoffreyBooth GeoffreyBooth force-pushed the synchronously-load-most-es-modules branch from 2bb88f9 to 9a7728c Compare April 1, 2026 02:17
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.70%. Comparing base (f8ee196) to head (9a7728c).

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #62530      +/-   ##
==========================================
- Coverage   89.71%   89.70%   -0.01%     
==========================================
  Files         695      695              
  Lines      214108   214174      +66     
  Branches    40995    41016      +21     
==========================================
+ Hits       192083   192124      +41     
- Misses      14075    14104      +29     
+ Partials     7950     7946       -4     
Files with missing lines Coverage Δ
lib/internal/modules/esm/loader.js 98.61% <100.00%> (-0.16%) ⬇️
lib/internal/modules/esm/module_job.js 93.35% <100.00%> (-3.21%) ⬇️
lib/internal/modules/run_main.js 97.83% <100.00%> (+0.19%) ⬆️

... and 28 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.

Copy link
Copy Markdown
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

esm Issues and PRs related to the ECMAScript Modules implementation. module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants