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.
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:
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:
unpkg.com/:package@:version/:file
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.
Conclusion
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:
- Demo: https://dmnsgn.github.io/es-modules-in-the-browser-almost-now/
- Sources: https://github.com/dmnsgn/es-modules-in-the-browser-almost-now
Some useful links:
- ExploringJS: 16. Modules
- ECMAScript modules in browsers
- unpkg getlibs
- SystemJS
- Package name maps proposal
- Tomorrow’s ECMAScript modules today!
Update: one step further with esm modules now working in Node.js without the experimental-modules flag. See https://github.com/nodejs/node/pull/29866.