JavaScript modules

Published · Tagged with ECMAScript ES2015

JavaScript modules are now supported in all major browsers!

This article explains how to use JS modules, how to deploy them responsibly, and how the Chrome team is working to make modules even better in the future.

What are JS modules? #

JS modules (also known as “ES modules” or “ECMAScript modules”) are a major new feature, or rather a collection of new features. You may have used a userland JavaScript module system in the past. Maybe you used CommonJS like in Node.js, or maybe AMD, or maybe something else. All of these module systems have one thing in common: they allow you to import and export stuff.

JavaScript now has standardized syntax for exactly that. Within a module, you can use the export keyword to export just about anything. You can export a const, a function, or any other variable binding or declaration. Just prefix the variable statement or declaration with export and you’re all set:

// 📁 lib.mjs
export const repeat = (string) => `${string} ${string}`;
export function shout(string) {
return `${string.toUpperCase()}!`;
}

You can then use the import keyword to import the module from another module. Here, we’re importing the repeat and shout functionality from the lib module, and using it in our main module:

// 📁 main.mjs
import {repeat, shout} from './lib.mjs';
repeat('hello');
// → 'hello hello'
shout('Modules in action');
// → 'MODULES IN ACTION!'

You could also export a default value from a module:

// 📁 lib.mjs
export default function(string) {
return `${string.toUpperCase()}!`;
}

Such default exports can be imported using any name:

// 📁 main.mjs
import shout from './lib.mjs';
// ^^^^^

Modules are a little different from classic scripts:

  • Modules have strict mode enabled by default.

  • HTML-style comment syntax is not supported in modules, although it works in classic scripts.

    // Don’t use HTML-style comment syntax in JavaScript!
    const x = 42; <!-- TODO: Rename x to y.
    // Use a regular single-line comment instead:
    const x = 42; // TODO: Rename x to y.
  • Modules have a lexical top-level scope. This means that for example, running var foo = 42; within a module does not create a global variable named foo, accessible through window.foo in a browser, although that would be the case in a classic script.

  • Similarly, the this within modules does not refer to the global this, and instead is undefined. (Use globalThis if you need access to the global this.)

  • The new static import and export syntax is only available within modules — it doesn’t work in classic scripts.

  • Top-level await is available in modules, but not in classic scripts. Relatedly, await cannot be used as a variable name anywhere in a module, although variables in classic scripts can be named await outside of async functions.

Because of these differences, the same JavaScript code might behave differently when treated as a module vs. a classic script. As such, the JavaScript runtime needs to know which scripts are modules.

Using JS modules in the browser #

On the web, you can tell browsers to treat a <script> element as a module by setting the type attribute to module.

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

Browsers that understand type="module" ignore scripts with a nomodule attribute. This means you can serve a module-based payload to module-supporting browsers while providing a fallback to other browsers. The ability to make this distinction is amazing, if only for performance! Think about it: only modern browsers support modules. If a browser understands your module code, it also supports features that were around before modules, such as arrow functions or async-await. You don’t have to transpile those features in your module bundle anymore! You can serve smaller and largely untranspiled module-based payloads to modern browsers. Only legacy browsers get the nomodule payload.

Since modules are deferred by default, you may want to load the nomodule script in a deferred fashion as well:

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

Browser-specific differences between modules and classic scripts #

As you now know, modules are different from classic scripts. On top of the platform-agnostic differences we’ve outlined above, there are some differences that are specific to browsers.

For example, modules are evaluated only once, while classic scripts are evaluated however many times you add them to the DOM.

<script src="classic.js"></script>
<script src="classic.js"></script>
<!-- classic.js executes multiple times. -->

<script type="module" src="module.mjs"></script>
<script type="module" src="module.mjs"></script>
<script type="module">import './module.mjs';</script>
<!-- module.mjs executes only once. -->

Also, module scripts and their dependencies are fetched with CORS. This means that any cross-origin module scripts must be served with the proper headers, such as Access-Control-Allow-Origin: *. This is not true for classic scripts.

Another difference relates to the async attribute, which causes the script to download without blocking the HTML parser (like defer) except it also executes the script as soon as possible, with no guaranteed order, and without waiting for HTML parsing to finish. The async attribute does not work for inline classic scripts, but it does work for inline <script type="module">.

A note on file extensions #

You may have noticed we’re using the .mjs file extension for modules. On the Web, the file extension doesn’t really matter, as long as the file is served with the JavaScript MIME type text/javascript. The browser knows it’s a module because of the type attribute on the script element.

Still, we recommend using the .mjs extension for modules, for two reasons:

  1. During development, the .mjs extension makes it crystal clear to you and anyone else looking at your project that the file is a module as opposed to a classic script. (It’s not always possible to tell just by looking at the code.) As mentioned before, modules are treated differently than classic scripts, so the difference is hugely important!
  2. It ensures that your file is parsed as a module by runtimes such as Node.js and d8, and build tools such as Babel. While these environments and tools each have proprietary ways via configuration to interpret files with other extensions as modules, the .mjs extension is the cross-compatible way to ensure that files are treated as modules.

Note: To deploy .mjs on the web, your web server needs to be configured to serve files with this extension using the appropriate Content-Type: text/javascript header, as mentioned above. Additionally, you may want to configure your editor to treat .mjs files as .js files to get syntax highlighting. Most modern editors already do this by default.

Module specifiers #

When importing modules, the string that specifies the location of the module is called the “module specifier” or the “import specifier”. In our earlier example, the module specifier is './lib.mjs':

import {shout} from './lib.mjs';
// ^^^^^^^^^^^

Some restrictions apply to module specifiers in browsers. So-called “bare” module specifiers are currently not supported. This restriction is specified so that in the future, browsers can allow custom module loaders to give special meaning to bare module specifiers like the following:

// Not supported (yet):
import {shout} from 'jquery';
import {shout} from 'lib.mjs';
import {shout} from 'modules/lib.mjs';

On the other hand, the following examples are all supported:

// Supported:
import {shout} from './lib.mjs';
import {shout} from '../lib.mjs';
import {shout} from '/modules/lib.mjs';
import {shout} from 'https://simple.example/modules/lib.mjs';

For now, module specifiers must be full URLs, or relative URLs starting with /, ./, or ../.

Modules are deferred by default #

Classic <script>s block the HTML parser by default. You can work around it by adding the defer attribute, which ensures that the script download happens in parallel with HTML parsing.

Module scripts are deferred by default. As such, there is no need to add defer to your <script type="module"> tags! Not only does the download for the main module happen in parallel with HTML parsing, the same goes for all the dependency modules!

Other module features #

Dynamic import() #

So far we’ve only used static import. With static import, your entire module graph needs to be downloaded and executed before your main code can run. Sometimes, you don’t want to load a module up-front, but rather on-demand, only when you need it — when the user clicks a link or a button, for example. This improves the initial load-time performance. Dynamic import() makes this possible!