All modern browsers landed native ECMAScript 6 modules support last year. I was excited to try them out on a real project and did not have to wait for too long. I started prototyping a web project for a client a few months ago and decided to use native modules as long as it does not prevent me from getting things done.
ES6 modules in browsers?
This post does not explain browser-native modules in detail. I recommend reading Jake Archibald’s excellent article if you want to know more. Here is an example with the essentials:
<script type="module" src="app.js"></script>
import lib from './lib.js' lib.doStuff()
- You will need a static web server (most likely you already have one) for development as ES6 modules can not be loaded directly from the file system.
- If modules are served from a web server with authentication enabled
(eg. HTTP basic) make sure the script tag has the
crossorigin="use-credentials"attribute to avoid surprises.
Using external dependencies
As long as you write your own modules and import them with ES6 module syntax, you are lucky. Eventually any non-trivial project will depend on external dependencies. And this is where things will become complicated.
- Written in ES6 syntax or older without using ES6 modules
- Written in ES6 and using ES6 modules
Before publishing to npm or a CDN libraries are compiled into one or more other formats that can (or can not) be consumed by browsers or certain versions of Node.js. The most common publishing formats are:
- Published as CommonJS
- Published as Universal Module Definition (UMD) or something else that even older browsers can directly use (or with a lightweight loader polyfill)
- Published as ES6 modules
Now comes the sad reality. Most of these formats can not be used with
the shiny new
import './app.js' syntax. Anything that is not in the
new module format simply does not work.
Unfortunately even most libraries authored or published in ES6 module
format will not work because they target
and rely on the Node.js ecosystem. Why is that a problem? Using bare
module paths like
import _ from 'lodash' is currently invalid,
browsers don’t know what to do with them.
Using UMD modules with
import is not possible either. Browsers throw
SyntaxError when attempting to parse a file as an ES6 module if it
In real life therefore:
- You will still need Webpack or something similar to load non-ES6 modules or rely on plain old script tags and global variables. Not pretty but might work until the dependencies are only a handful.
- Libraries published in browser-friendly ES6 format can be used
directly from a CDN or from a local
node_modulesfolder when served by the web server. In that case you will have to spell out the whole path including
Library usage examples:
// Some authors already publish in browser friendly ES6 format. import Vue from 'https://firstname.lastname@example.org/dist/vue.esm.browser.js' // Lodash in ES6 format is published under a different name. When // using npm, the whole node_modules folder has to be exposed by the // static web server. import camelCase from './node_modules/lodash-es/camelCase.js' // Other libraries are authored in ES6 module format but use bare // module paths which does not work in browsers. CDNs like Unpkg can // help by expanding the modules. import scale from 'https://email@example.com/index.js?module' // Sadly there is no module expansion when installed with npm, this // will NOT work. import scale from './node_modules/d3-scale/index.js'
See these ES6 module loading examples in your browser.
Loading templates or CSS with import
There is hope though. One day browsers will implement the
System.loader API which would
allow adding custom module loaders but the standard is not even ready
to be implemented.
Custom loaders with Service Workers
Unfortunately Service Workers are not yet as widely implemented by browsers as ES6 modules but I was too curious and implemented a rudimentary text file loader. Check out the examples and more specifically the worker itself:
This idea can be adopted to load CSS into the page or compile templates on the fly. Beware however, Service Workers is a very new technology.
Fun fact: as of writing you can not write Service Workers as ES6 modules.
Widely used bundling tools like Webpack or Browserify will have no
problem transpiling ES6 modules. Likely you will transpile the code
into pre-ES6 modules, therefore
<script type="module"> will have to
be rewritten to plain old script tags. Check the documentation of your
tool to see how that is possible.
Since browser-native modules are a very new technology expect development tools to choke on them here-and-there, one such example is Karma I had trouble with.
Another challenge can be supporting old browsers. If you develop using ES6 native modules your raw code will only work in the latest browser versions.
Sharing code in ES modules in Node.js can be tricky too. Native ES6
modules in Node are behind the
--experimental-modules flag and require naming modules differently
.mjs extension. You can use
(surprise!) “ES6-style module-loading may not function as expected”.
You can start using ES6 modules for experiments, prototyping and small projects today. Anything beyond that will eventually require more tooling. In fact introducing Webpack in development is soon going to happen on my current client project.
My wishlist for the future:
- First class ES6 native module support in build tools so that we can still ship optimized and backwards compatible code in production.
- Custom loaders for importing templates, CSS and others as modules so that we can use the same approach for modularizing my code everywhere.
- Maybe a smart static HTTP/2 server that allows importing large dependency trees (many small modules) efficiently without bundling so that there is no need for insanely complicated development servers. See this interesting topic on ES Discuss.
I used these articles while researching for this post and highly recommend them to learn further details about native ES6 modules in browsers and Node.js.