ES modules in the browser — almost — now

With every major browser vendors now supporting ES modules, it is time to have a closer look at the practical usage and applicability in production environments. We’ll first introduce some core concepts on the topic to then reflect on the main bottleneck currently slowing down a broader adoption.

Image for post
Image for post

Note: I assume you know about different Javascript module systems in general. If not, Exploring JS by Axel Rauschmayer is one of the good places to start.

The basics of ES modules

Let’s keep it simple for the sake of this article. I won’t cover them in depth, but if you are interested, you can refer to this extensive article from Jake Archibald on the subject:

The recipe is simple:

  • Write some modularised JavaScript inline in a <script> tag or link an external script via a src attribute.
  • Add a type=module attribute on the script tag.
  • Use nomodule to provide a fallback for browsers with no support. The ones that do support them will ignore any script with this attribute.
  • Write code like it’s 2020 and you don’t need a compilation/transpilation step anymore (subject to terms conditions or limitations™, keep reading).

Sounds easy, right? But wait, it gets more complicated as soon as you start using external dependencies.

The bare import problem

There is one catch for those of us who are used to the Node.js ecosystem and tools relying on it (gulp/browserify/webpack/JSPM/rollup/you name it): you can’t use a bare import eg. doing import { clamp } from “lodash” will fail in the browser. If you try in Chrome, you’ll get this pretty self explanatory error:

Image for post
Image for post
Relative references must start with either “/”, “./” or “../”

How come?

Other specifiers are reserved for future-use, such as importing built-in modules.

“Why not bridging with the Node.js module resolution algorithm then?” one might say.

The difference lies in the fact that in a browser you can’t afford to blindly check for a file existence: a single HTTP request is all that it should take — as opposed to a bunch of file system reads. Also, how would you translate the Node.js conventions, such as using a node_modules folder, and all the complexity between global and local packages?

The possible solution(s) nowadays

In 2018, it is possible to use npm packages directly as ES modules in your bundled application, but it requires a bit of cerebral gymnastic.

But wait a second, why would you still need a module bundler if your browser provides a module system you can use directly? Well, here are a few reasons:

  • Not every module is written using the ES modules system (hey Common.js, how are you holding up today?)
  • Some modules rely on syntaxes/features not available everywhere yet (say, Rest/Spread Properties) and polyfill-ing or transpiling might be required in order to use them
  • Plenty of applications are relying on loading various kind of assets (hello Webpack)
  • Optimisations: if HTTP2 gives you parallel requests, it doesn’t change the fact that your modules could/should benefit from minification

Now, what can we do about the bare import specifier issue? Let’s try to think pragmatically from the perspective of people who are making an extensive use of module bundlers via Node.js in production. Here are some solutions:

Keep bundling (sad face)

Well, that defeats the whole purpose of a modular system requiring modules in separate files, but you could still bundle all your application in a single file (or chunks) including your external dependencies. That’s probably your only option if you need to support a wide range of browsers.

Rewrite the import paths

One obvious solution for us, who are used to our code being transformed, is to rewrite the paths linking to external dependencies. This presuppose that all of them are available as ES modules somewhere.

If you are fine with this restriction, you might want to give a go at unpkg:

unpkg is a fast, global content delivery network for everything on npm. Use it to quickly and easily load any file from any package using a URL like:

Basically, a CDN for your node_modules folder based on SystemJS. What is appealing about it is the “There is nothing to install” concept introduced by the getlibs endpoint:

A single script import seems to be solving our main problem and has some neat features like babel integration (also typescript, yay!) to transpile on the fly in Service Workers to make it not so slow. But to be clear:

It is not a good idea to transpile your code in-browser in production (unless it is only required for a small number of older browsers — but we are not there yet :-)).

If you are not relying on too many libs and they expose their ES module sources, you could also implement this in a Service Worker yourself but you have to unsure it is registered before importing your modules (on first load, you’ll have to refresh the page in the following case):

However, these “solutions” all have problems that lead to the following proposal.

Package name maps 🎉

As explained in the repo by Domenic Denicola, the objective is to fix the bare import specifier issue with an ahead-of-time computed mapping. A bit like the previous example with the service worker where you define a Map and rewrite the URL:

The above is a tentative idea. The actual implementation might differ from this snippet once these brilliant people figure out what works best in the ECMAScript world of today (the Issues section contains a lot of interesting discussions on the why and how). Nevertheless, it seems to be a very promising proposal that could smoothen the transition from the world of single bundle file to modular system.

Update: an experimental polyfill is in the making.


The shift towards ES modules in the browser has really happened. They have learned from other popular module systems such as AMD and Common.js to become the de facto standard for the future of JavaScript. With Node.js being omnipresent in the frontend workflow, CJS modules remain a serious component but projects like the esm package bring us one step closer to a full ES modules world.

We almost got it all figure it out. At the moment, you can use one of the above solutions — probably unpkg — granted that:

  • you work with light scopes that don’t include old browsers (IE11 and all of the awkward mobile browsers)
  • you only have a few and modern dependencies
  • you don’t need the fastest response time (for educational purpose, for instance)

I am pretty sure this article will become obsolete very quickly, hopefully for the best.

If you want to try the different methods explained above, check out:

Some useful links:

Update: one step further with esm modules now working in Node.js without the experimental-modules flag. See

Written by

Tech Lead/Creative Developer @variable_io , Generative Artisan, Cheese Enthusiast.

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