Node.js 25.4: require(esm) and the module compile cache are now stable

Node.js 25.4.0 shipped on January 19 on the Current line. At first glance the changelog looks routine: dependency updates, new collaborators, a few utilities. But one group of commits deserves attention: several features that had long been marked experimental or in testing were declared stable at once. The headline one is require(esm), and that closes a long chapter in the ecosystem’s move to ES Modules.

What became stable

This release marks as stable:

  • require(esm): loading an ES module with require() from CommonJS code;
  • the module compile cache;
  • --heapsnapshot-near-heap-limit;
  • --build-snapshot and --build-snapshot-config;
  • v8.queryObjects();
  • crypto.hash, whose documentation now marks it as stable.

Alongside that, the release adds the --require-module and --no-require-module flags. They are new, not a stability change, and give explicit control over require(esm) behavior.

It is worth noting who signed many of these commits: Joyee Cheung is listed as the author of the stabilization of require(esm), the compile cache, --build-snapshot, v8.queryObjects() and the new flags. This is not a random collection of changes but a coordinated effort to take the experimental label off features many people were already using in production. For teams deciding what to adopt, “stable” is exactly the signal that was missing.

Why a stable require(esm) matters

For years, library authors faced a dilemma. Publishing ESM only meant breaking users stuck on CommonJS, since require() could not load ES modules. Publishing both formats (the famous “dual package”) meant maintaining duplicate builds and dealing with duplicate-instance problems.

With a stable require(esm), the path gets simpler: a package can be ESM only and still be consumed by CommonJS code via require(). In practice, that removes many maintainers’ excuse for continuing to ship CommonJS.

// CommonJS code loading a package that only ships ESM
const lib = require("esm-only-package");

The caveat is that this applies to the Current line. If you are on an LTS release, check the feature’s status in your version before assuming the same behavior, and libraries that want to support older Node versions still need to be careful.

Performance and diagnostics

The other stable items are less flashy but useful in production:

  • Compile cache: storing module compilation results on disk speeds up repeated startups, which makes a difference for CLIs and serverless environments with frequent cold starts. Stable means you can adopt it without fear of the API changing.
  • --build-snapshot: generating a startup snapshot of the application is another weapon against startup time.
  • --heapsnapshot-near-heap-limit and v8.queryObjects(): memory diagnostics tools. The first writes a heap snapshot when the process approaches its heap limit, which is exactly the moment you want to capture when hunting a leak.

Other additions

A few SEMVER-MINOR items worth knowing:

  • Subpath imports with #/: internal imports that start with #/ are now allowed, a common pattern for pointing at the project root via the imports field in package.json.
  • http.setGlobalProxyFromEnv(): a new function to configure a global proxy from environment variables.
  • util.convertProcessSignalToExitCode: a new utility for converting process signals into exit codes.
  • events.listenerCount() now accepts EventTargets.
  • queueMicrotask preserves AsyncLocalStorage only when needed.

On the dependency side, root certificates were updated to NSS 3.117, npm moved to 11.7.0 and the bundled SQLite to 3.51.1. The docs also now treat synchronous module hooks as a release candidate.

My take

Node 25.4 is one of those releases that looks small and matters. Stabilizing require(esm) is the kind of change that, little by little, simplifies the whole ecosystem: fewer dual builds, fewer convoluted exports configurations, fewer ERR_REQUIRE_ESM errors in legacy projects. If I maintained a library today, I would start planning an ESM-only release, knowing that full adoption depends on users reaching Node versions with this feature. Support for #/ is also welcome: it is a simple pattern that removes the need for aliases configured in every tool.

Sources