Using ES Modules in the Browser Today

Share this article

ES Modules

This article will show you how you can use ES modules in the browser today.

Until recently, JavaScript had no concept of modules. It wasn’t possible to directly reference or include one JavaScript file in another. And as applications grew in size and complexity, this made writing JavaScript for the browser tricky.

One common solution is to load arbitrary scripts in a web page using <script> tags. However, this brings its own problems. For example, each script initiates a render-blocking HTTP request, which can make JS-heavy pages feel sluggish and slow. Dependency management also becomes complicated, as load order matters.

ES6 (ES2015) went some way to addressing this situation by introducing a single, native module standard. (You can read more about ES6 modules here.) However, as browser support for ES6 modules was initially poor, people started using module loaders to bundle dependencies into a single ES5 cross-browser compatible file. This process introduces its own issues and degree of complexity.

But good news is at hand. Browser support is getting ever better, so let’s look at how you can use ES6 modules in today’s browsers.

The Current ES Modules Landscape

Safari, Chrome, Firefox and Edge all support the ES6 Modules import syntax. Here’s what they look like.

<script type="module">
  import { tag } from './html.js'

  const h1 = tag('h1', '👋 Hello Modules!')
  document.body.appendChild(h1)
</script>
// html.js
export function tag (tag, text) {
  const el = document.createElement(tag)
  el.textContent = text

  return el
}

Or as an external script:

<script type="module" src="app.js"></script>
// app.js
import { tag } from './html.js'

const h1 = tag('h1', '👋 Hello Modules!')
document.body.appendChild(h1)

Simply add type="module" to your script tags and the browser will load them as ES Modules. The browser will follow all import paths, downloading and executing each module only once.

ES modules: Network graph showing loading

Older browsers won’t execute scripts with an unknown “type”, but you can define fallback scripts with the nomodule attribute:

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

Requirements

You’ll need a server to be able to fetch with import, as it doesn’t work on the file:// protocol. You can use npx serve to start up a server in the current directory for testing locally.

If you want to load ES modules on a different domain, you’ll need to enable CORS .

If you’re bold enough to try this in production today, you’ll still need to create separate bundles for older browsers. There’s a polyfill available at browser-es-module-loader which is following the spec. However, this isn’t recommended for production at all.

<script nomodule src="https://unpkg.com/browser-es-module-loader/dist/babel-browser-build.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader"></script>
<script type="module" src="./app.js"></script>

Performance

Don’t throw away your build tools like Babel and Webpack just yet, as browsers are still implementing ways to optimize fetching. Still, there are performance pitfalls and gains to be had in the future with ES Modules.

Why We Bundle

Today we bundle our JavaScript to reduce the number of HTTP requests being made, as the network is often the slowest part of loading a web page. This is still a very valid concern today, but the future is bright: ES Modules with HTTP2’s ability to stream multiple assets with server push and browsers implementing preloading.

Preloading

link rel=”modulepreload” is coming soon to a browser near you. Rather than having the browser resolve all the module imports one by one, producing a network waterfall like this …

<script type="module" src="./app.js"></script>
---> GET index.html
<---
    ---> GET app.js
    <---
        ---> GET html.js
        <---
            ---> GET lib.js
            <---

… you’ll be able to tell the browser up front that the pages require html.js and lib.js, keeping that waterfall under control:

<link rel="modulepreload" href="html.js">
<link rel="modulepreload" href="lib.js">
<script type="module" src="./app.js"></script>
---> GET /index.html
<---
      ---> GET app.js
      ---> GET html.js
      ---> GET lib.js
      <---
      <---
      <---

HTTP2 with Server Push

HTTP2 is capable of pushing multiple resources in a single response compared to HTTP1.1, which can only deliver one. This will help keep the number of round trips over the network to a minimum.

In our example, it would be possible to deliver index.html, app.js and html.js in a single request:

---> GET /index.html
<--- index.html
<--- app.js
<--- html.js
<--- lib.js

Caching

Delivering multiple smaller ES modules may benefit caching as the browser will only need to fetch the ones that have changed. The problem with producing large bundles is that if you change one line, you invalidate the whole bundle.

async / defer

ES modules are not render blocking by default, like <script defer>. If your modules don’t need to be executed in the same order they are defined in the HTML, you can also add async to execute them as soon as they’re downloaded.

Libraries

Popular libraries are starting to be published as ES modules now, however they’re still targeting bundlers and not direct imports.

This humble little import triggers a waterfall of 640 requests:

<script type="module">
  import _ from 'https://unpkg.com/lodash-es'
</script>

How about if we do the right thing and just import the one function we need? We’re down to a mere 119 requests:

<script type="module">
  import cloneDeep from 'https://unpkg.com/lodash-es/cloneDeep'
</script>

This is just an example to demonstrate that lodash-es is not built to be loaded directly in the browser yet. To do that, you’ll still need to create your own bundle with ES modules as the target.

Browser Support

As the following table shows, browser support for ES modules is good (and getting better all the time).

Can I Use es6-module? Data on support for the es6-module feature across the major browsers from caniuse.com.

The time to start experimenting with ES modules in the browser is now. Soon enough, you’ll be able to use them in all modern browsers without a transpiler or bundler, should you wish.

Frequently Asked Questions (FAQs) about ES Modules

What are the benefits of using ES Modules in JavaScript?

ES Modules, also known as ECMAScript Modules, are a standardized format for organizing and structuring JavaScript code. They offer several benefits over traditional script loading. Firstly, they allow for static analysis, meaning tools can determine imports and exports at compile time (as opposed to runtime). This leads to better performance optimization and an improved ability to identify and eliminate unused code. Secondly, ES Modules support both synchronous and asynchronous loading, providing greater flexibility. Lastly, they promote better code organization and structure, making code easier to maintain and understand.

How do ES Modules differ from CommonJS Modules?

ES Modules and CommonJS Modules are two popular module systems in JavaScript. The key difference between them lies in their loading mechanism. CommonJS Modules use a synchronous loading mechanism, which means the modules are loaded one after the other. On the other hand, ES Modules support both synchronous and asynchronous loading, allowing for more efficient code execution. Additionally, ES Modules support static analysis, which is not possible with CommonJS Modules.

Can I use ES Modules in all browsers?

As of now, all modern browsers support ES Modules. However, older versions of browsers may not support them. You can check the compatibility of ES Modules with different browsers on websites like “Can I use”. It’s also worth noting that ES Modules are not supported in Node.js by default, but this can be enabled with certain flags or by using the .mjs file extension.

How do I import and export functions using ES Modules?

Importing and exporting functions using ES Modules is straightforward. To export a function, you can use the ‘export’ keyword before the function declaration. To import a function, you can use the ‘import’ keyword followed by the function name and the path to the module. Here’s an example:

// exporting a function
export function myFunction() {
// function body
}

// importing a function
import { myFunction } from './myModule.js';

What is the difference between default and named exports in ES Modules?

In ES Modules, you can use either default or named exports. A module can have only one default export, but multiple named exports. When importing a default export, you don’t need to use curly braces, and you can name the import whatever you like. On the other hand, when importing a named export, you need to use the exact name of the export in curly braces. Here’s an example:

// default export
export default function() { ... }

// named export
export function myFunction() { ... }

// importing default export
import myFunction from './myModule.js';

// importing named export
import { myFunction } from './myModule.js';

How can I use ES Modules in Node.js?

As of Node.js version 13.2.0, ES Modules are supported by default. For versions below 13.2.0, you can enable ES Modules by setting the “type” field in your package.json file to “module”. Alternatively, you can use the .mjs file extension for your module files. To import a module, you can use the ‘import’ keyword, just like in the browser.

Can I mix ES Modules and CommonJS in the same project?

Yes, you can use both ES Modules and CommonJS in the same project. However, it’s important to note that they have different semantics and behave differently in certain scenarios. For instance, ES Modules support static analysis and asynchronous loading, which are not supported by CommonJS. Therefore, while it’s technically possible to mix them, it’s generally recommended to stick to one module system for consistency and predictability.

What is the future of ES Modules?

ES Modules are the official standard for JavaScript modules and are supported by all modern browsers. They offer several advantages over traditional script loading and other module systems, such as static analysis and better code organization. As such, it’s expected that their usage will continue to grow in the future. Additionally, ongoing improvements in tooling and browser support will likely make ES Modules even more powerful and flexible.

How can I convert my CommonJS Modules to ES Modules?

Converting CommonJS Modules to ES Modules involves changing the ‘require’ and ‘module.exports’ syntax to ‘import’ and ‘export’ syntax. Here’s an example:

// CommonJS
const myModule = require('./myModule');
module.exports = myFunction;

// ES Modules
import myModule from './myModule';
export default myFunction;

Keep in mind that this is a simplified example and the actual conversion process might involve more steps, depending on the complexity of your code.

Are there any tools to help me work with ES Modules?

Yes, there are several tools that can help you work with ES Modules. Babel is a popular tool that can transpile ES6 code (including ES Modules) to ES5, making it compatible with older browsers. Webpack is another tool that can bundle your modules into a single file for production. Additionally, tools like ESLint can help you enforce best practices and catch potential errors in your code.

Mark BrownMark Brown
View Author

Hello. I'm a front end web developer from Melbourne, Australia. I enjoy working on the web, appreciate good design and working along side talented people I can learn from. I have a particular interest in visual programming so have fun working with SVG and Canvas.

es modulesjavascript moduleslearn-modernjsmodernjsmodernjs-hub
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week