Skip to content Paweł Grzybek

Native ECMAScript modules in the browser

Three things that I wish I could ditch from my everyday front-end workflow: CSS preprocessors, JavaScript transpilers and module bundlers. Let me elaborate…

I love Sass but wouldn’t it be cool to have its power built into CSS? The good news is native custom properties are much more powerful than statically declared ones from Sass. Mixins are amazing — unfortunately the @apply rule has been abandoned by the spec creator but I’m sure we will get some decent replacement at some point. The vendor prefixes imbroglio is over and will never come back. With all those goodies I would say that the first of my dreams is fulfilled.

When Babel came around it was like living in the future. We were able to use modern features that browsers didn’t support at the time. Things have changed though. Nowadays, browsers have really strong support for present-day ingredients. Have a look at your Gulp build task or Webpack config — there’s a good chance that you don’t need to transpile your code anymore. The second item in my wish list has become a reality.

Safari 10.1 brings me hope that one day I will check the box next to the last item that I would like to forget — module bundlers.

JavaScript modules recap

Before 2015 JavaScript didn’t have a native way of working with modular codebases. For years web developers managed to find a number of ways to implement it: splitting the codebase into separated files and scopes, using AMD (Asynchronous module definition) with file loaders like RequireJS or making the Node-style CommonJS work in the browser via libraries like Browserify.

Finalised in June 2015 the spec for the 6th edition of JavaScript changed things a lot. One of the many amazing things that it brought was a native way of working with modules. It turned out that it was immensely hard to implement on the web platform — so module bundlers like Webpack came into the game. They allowed us to write code in a modern way and spit out a bundled script understandable by the browser.

Safari 10.1 is the first browser that has received 100% complete coverage for ECMAScript 2015 features. Really great work Apple (Service Workers next please). It means that it is the first browser that allows us to use native modules. Another implementation landed on Google Chrome 61, Firefox 54 – behind the dom.moduleScripts.enabled setting in about:config and Edge 16. Let’s have a look at the nitty-gritty.

Working with modules in the browser

If you have ever used import and export in your project, migrating this concept to a client won’t be a challenging task. If you have never worked with these concepts before, give this chapter of Dr. Axel Rauschmayer’s book a quick read.

Nothing works better than a practical example. Let’s create a script that prints a stylish log message into the console. Let’s split it out into two files — index.js as an entry point and print.js that holds a reusable print function (module). It goes something like this (sorry for the issue with ES2015 syntax highlighting).

// index.js

import print from './print.js';
print('Native ECMAScript modules in the browser');
// print.js

export default message => {
  console.log(
    `%c ${message}`,
    `
    color: hotpink;
    font-family: Comic Sans MS;
    font-size: 1.5rem;
    `
  );
};

In the world of module bundlers we need to run this set of files through it to get a bundled file that contains our script and some boilerplate on top of it. Then we have to smash a script tag with a src attribute that points to this file and voilà — it works. Now let’s forget about bundling for a sec and take advantage of a browser that can finally resolve all dependencies for us. This is how to do it baby…

<script src="./index.js" type="module"></script>

Yes the essence lies here — type="module". According to the spec these few characters tell the browser that it can be used to include external module scripts. This is exactly what we need! Pay attention to the import path inside the index.js file — it needs to be a concrete path to an imported file (including the extension). Let’s open it in the browser (more about the support later on).

Native ECMAScript modules in Safari

The browser managed to resolve the dependency of the print.js file. No Webpack magic here! Beautiful, isn’t it?

But my browser doesn’t… Yes it does!

A quick word about support of native modules across the browsers and some possible solutions. At the time of writing this article it’s looking like this:

  • Chrome Canary 61
  • Firefox 54 – behind the dom.moduleScripts.enabled setting in about:config
  • Edge 16
  • Safari — hell yeah!
Browser support for Native ECMAScript modules

It doesn’t look very promising and is definitely not ready to use in production. There is a hope though! Let’s have a look at what the console of any browser that lacks native module support shows when I open our pretty print example.

Native ECMAScript modules in Google Chrome

So what happened here? NOTHING! Absolutely nothing. Because Google Chrome doesn’t support JavaScript modules it totally ignores it. You probably know where I’m going with this.

<script src="./index.js" type="module"></script>
<script src="./bundle.js" nomodule></script>

Yes! When modules are not supported natively let’s use something that definitely works. As a fallback script let’s use an output file from our module bundler of choice. It works perfectly well now in any browser that doesn’t have a clue how to handle JavaScript modules. Make sure that you are not duplicating the same functionality for browsers that do support it — it is exactly for this reason the nomodule attribute has been recently added to the spec. Let’s have a look at the results in Safari and Google Chrome (or any other browser that doesn’t support modules) now…

Native ECMAScript modules in Safari Technology Preview and Google Chrome

Last word about JavaScript modules

That’s it. I hope that you are as equally excited about native modules as I am. I would like to leave you here with two conclusions.

The last few years introduced many tools and added massive complexity to front-end development. The rapid change of the JavaScript workflow constantly attracts new developers and scares others. Module bundling is one of those things that adds gigantic confusion — hopefully in this post I have demonstrated to you that it won’t last forever. Things just became much simpler.

The thing that I love about the web is its unpredictability and active transformation. One day, something becomes much simpler and the next day a new idea comes in to confuse our brains even more. I can’t wait to see what the future has to offer…

Comments

  • j
    jose

    I really like that the import has to be a real directory path. It feels more mechanical that way, like I know what's going on. I always found those imports nebulous and magical.

    Looking at deployment, wouldn't bundling and gzipping still be a good thing to reduce the number of requests, helping slower connections?

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      I totally agree! Absolute paths give me more understanding of file structure.

      Bundling and gzipping definitely makes sense nowadays. HTTP2 may change a bit on this territory. Time will show!

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • D
    Damien Lebrun

    It won't be the end of bundler. You will still need a fail back bundle and for browser supporting ES modules, bundles will be more efficient to serves than a thousand modules; even with http2, loading too many modules is slow and small files cannot be efficiently compressed.

    Bundling will probably become more complicated since we will be able lazy load module on demand and we will have to decide how to split and group the code base.

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • D
      Dmitri

      Bundling and compiling will always have their uses. But it is always good to have a choice whether to use them or not.

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • D
    Dmitri

    It might be just me, but I personally find the syntax

    `export default message => { .... }`

    to be non-standard and more confusing comparing to the regular CommonJS

    `module.exports = message => {...}`

    The latter is a standard property assignment, whereas the former looks like nothing else in JavaScript.

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      It will take a while to grasp some familiarity in this syntax but it will become a widely used syntax soon. It is import / export from ES2015. If you are looking for some more familiar looking code, this will do almost the same thing as the example from the article...


      function prettyPrint(message) {
      console.log(
      `%c ${message}`,
      `
      color: hotpink;
      font-family: Comic Sans MS;
      font-size: 1.5rem;
      `
      );
      };

      export default prettyPrint;

      Looking any better?

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
      • D
        Dmitri

        The last line has 3 keywords one after another, that never occurs in JavaScript otherwise. To me something like

        export.default = prettyPrint;

        would read much more natural.
        I've seen the syntax, I just don't understand the reason to make it look non-JS-native.

        👆 you can use Markdown here

        Your comment is awaiting moderation. Thanks!
        • Pawel Grzybek
          Pawel Grzybek

          How about named exports? It is just a JavaScript — its not some obscure framework on top of it...


          export { prettyPrint };

          👆 you can use Markdown here

          Your comment is awaiting moderation. Thanks!
          • D
            Dmitri

            It is tolerable but

            `module.exports = { prettyPrint };

            feels more native and is supported by the current LTS NodeJS v6
            without any additional tools.

            I have found my code looking cleanest by only exporting stateless functions with CommonJS, and for those use cases I am not aware of any compelling advantage offered by the ES6 modules over CommonJS. Are you aware of any?

            👆 you can use Markdown here

            Your comment is awaiting moderation. Thanks!
            • Pawel Grzybek
              Pawel Grzybek

              The only one that comes to my mind it fact that it is a part of a spec and eventually all environments (browser, node etc...) will support it as all of them just catching up the spec coverage.

              But I get your point 100%. It is very subjective tho. Will see what the future will show us :)

              👆 you can use Markdown here

              Your comment is awaiting moderation. Thanks!
              • D
                Dmitri

                It doesn't seem so easy though, looks like it will take a while for the Node:

                https://medium.com/the-node...

                👆 you can use Markdown here

                Your comment is awaiting moderation. Thanks!
  • D
    Dan Dascalescu

    You still need a bundler to import npm packages that don't have ES6 native modules (which is the vast majority of them) because there's no way to import CommonJS modules in the browser natively.

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Of course you need them now. This article is a little bit optimistic look at the future. We will get there one day. Have a great day Dan and thanks for reading.

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • M
    Mihail Malostanidis

    Shouldn't you still have a build step that affects all of your files to:
    1) Compile typescript/flow
    2) Compile JSX/Vue
    3) !!! Bundle everything and codesplit it appropriately for the client and not for the developer !!!
    4) Eliminate unused JS and CSS (especially from node_modules)
    5) Minify all files (strip comments, optimize for gzip, mangle names)
    6) Rewrite paths
    7) Add subresource integrity
    8) Add hashes to version static resources

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!

Leave a comment

👆 you can use Markdown here

Your comment is awaiting moderation. Thanks!