ES Modules in Node Today!

👉 Update: The “@std/esm” loader is now “esm”. Read more here.

John-David Dalton
Aug 10, 2017 · 5 min read

I’m excited to announce the release of (standard/esm), an opt-in, spec-compliant, ECMAScript (ES) module loader that enables a smooth transition between Node and ES module formats with near built-in performance! This fast, small, zero-dependency package is all you need to enable ES modules in Node today 🎉🎉🎉

Image for post
Image for post
@std/esm used in the Node REPL after `npm --save @std/esm`

A tale of two module formats

With ESM , attention is turning to Node’s future ESM support. Unlike browsers, which have an out-of-band and no prior module format, support for ESM in Node is a bit more…prickly. Node’s legacy module format, a (CJS) variant, is a big reason for Node’s popularity, but CJS also complicates Node’s future ESM support. As a refresher, let’s look at an example of both module syntaxes.


const a = require("./a")
module.exports = { a, b: 2 }


import a from "./a"
export default { a, b: 2 }

Note: For more in-depth comparisons see excellent .

Because CJS is not compatible with ESM, a distinction must be made. After much discussion, Node has using the “.mjs” (modular JavaScript) file extension to signal the “module” parse goal. Node has a history of processing resources by file extension. For example, if you require a .json file, Node will happily load and JSON.parse the result.

ESM support is tentatively slated to land, unflagged, in Node 10 anytime between . This puts developers, esp. package authors, in a tough spot. They could choose to:

  • Go all in, shipping only ESM, and alienate users of older Node versions
  • Wait until 2020, after , to go all in
  • Ship ESM and transpiled CJS sources, inflating package size and shouldering the responsibility of ensuring 1:1 behavior

None of those choices seem super appealing. The ecosystem needs something, that meets it where it is, to span the CJS to ESM gap.

Bridge building

Enter the loader, a user-land package designed to bridge the module gap. Since Node ES2015 features, is free to focus solely on enabling ESM.

The loader stays out of your way and tries to be a good neighbor by:

  • Not polluting stack traces
  • Working with your existing tools like and .
  • Playing well with other loaders like
    (using .babelrc )
  • Only processing files of packages that opt-in with a @std/esm configuration object or @std/esm as a dependency, dev dependency, or peer dependency in their package.json
  • Supporting versioning
    (i.e. package “A” can depend on one version of @std/esm and package “B” on another)

Unlike existing ESM solutions which require shipping transpiled CJS, performs minimal, inline source transformations on demand, processing and caching files at runtime. Processing files at runtime has a number of advantages.

  • Only process what is used, when it’s used
  • The same code is executed in all Node versions
  • Features are configurable by module consumers
    (e.g. module “A” consumes module “C” with the default config while module “B” consumes module “C” with cjs compat rules enabled)
  • More compliance opportunities
    (i.e. can enforce for environment variables, error codes, path protocol and resolution, etc.)

Standard features

Defaults are important. The loader strives to be as spec-compliant as possible while following .

Out of the box just works, no configuration necessary, and supports:


Developers have strong opinions on just about everything. To accommodate, @std/esm allows with the "@std/esm" package.json field. Options include:

  • Enabling unambiguous module support (i.e. files with at least an import, export, or "use module" pragma are treated as ESM)
  • Supporting of CJS modules
  • Top-level await in main modules


Before I continue let me qualify the following section:

It’s still super early, mileage may vary, results are hand wavey, etc. 👋

Testing was done using Node 9 compiled from , which enables built-in ESM support. I measured the taken to load the 643 modules of , converted to .mjs, against a baseline run loading nothing. Keep in mind the cache is good for the lifetime of the unmodified file. Ideally, that means you’ll only have a single non-cached load in production.

  • Loading CJS equivs was ~0.28 milliseconds per module
  • Loading built-in ESM was ~0.51 milliseconds per module
  • First no cache run was ~1.56 milliseconds per module
  • Secondary cached runs were ~0.54 milliseconds per module

Initial results look very promising, with cached loads achieving near built-in performance! I’m sure, with your help, parse and runtime performance will continue to improve.

Getting started

Run npm i esm in your app or package directory.

There are two ways to enable ESM with esm.

  1. Enable esm for packages:
    Use esm to load the main ES module and export it as CommonJS.


// Set options as a parameter, environment variable, or rc file.
require = require("esm")(module/*, options*/)
module.exports = require("./main.js")


// ESM syntax is supported.
export {}

2. Enable esm for local runs:

node -r esm main.js

Meteor’s might

The loader wouldn’t exist without , creator of the compiler from which is forked. He’s proven the loader implementation , since May 2016, in tens of thousands of Meteor apps!

All green thumbs

Even though has just been released it’s already had a positive impact on several related projects by:

What’s next

Like many developers, I want ES modules yesterday. I plan to use in 5 to not only transition to ESM but also leverage features like gzip module support to greatly reduce its package size.

The loader is . It’s my hope that others are as excited and as energized as I am. ES modules are here! This is just the start. What’s next is up to you. I look forward to seeing where you take it.

Final Thought

While this is not a Microsoft release, we’re proud to have a growing number of core contributors to fundamental JavaScript frameworks, libraries, and utilities at Microsoft. Contributors like of , of , of , of , of , of , and of , to name a few, who in addition to their roles at Microsoft, are helping shape the future of JavaScript and the web at large through standards engagement and ecosystem outreach. I’m happy to guest post this news on the Microsoft Edge blog to share our enthusiasm with the community!

Web Dev @ Microsoft

A place for developers from across Microsoft to share ideas…

John-David Dalton

Written by

JavaScript tinkerer, bug fixer, & benchmark runner • Creator of lodash • Former Chakra Perf PM • Current Web Apps & Frameworks PM @Microsoft. Opinions are mine.

Web Dev @ Microsoft

A place for developers from across Microsoft to share ideas and best practices

John-David Dalton

Written by

JavaScript tinkerer, bug fixer, & benchmark runner • Creator of lodash • Former Chakra Perf PM • Current Web Apps & Frameworks PM @Microsoft. Opinions are mine.

Web Dev @ Microsoft

A place for developers from across Microsoft to share ideas and best practices

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store