Native ECMAScript modules: nomodule attribute for the migration

Serg Hospodarets Blog

In one of my previous articles Native ECMAScript modules: the new features and differences from Webpack modules we attempted to detect if the browser supported ES modules. We needed this to determine either, to execute a bundled (classic) file or a script which uses the native ECMAScript module features.

We managed to achieve this, but in reality how it was achieved isn’t ideal. The community have since come together to propose an alternative, the nomodule script attribute.

The problem

The initial proposal started from a discussion and the requests from Google and Facebook developers how to start the migration to ES modules (ESM). The idea is that you create two scripts:

  1. one is bundled with import/ export statements resolved.

  2. and the second one, which expects the ES modules support in the browser.

Depending on the ESM support browser executes one of them. The case is straightforward and should be used for the progressive enhancements etc.

To execute a script as a module when in the browser, we need to add the type="module" attribute to it.
Browsers without ESM just ignore these scripts, as the type="module" attribute is unknown for them.

But currently there is no boolean flag or an easy way to detect if the browser supports the ES modules.
So, how do we make new browsers with ESM support ignore the additional scripts which shoudn’t be executed?

As was mentioned, I proposed to create an empty ES script and include it to the browser using the Blob() API. Then we just have to wait till it’s loaded. If the script is loaded successfully, the browser supports ES modules, otherwise wait for a period of time, reject and load the alternative script… Not the best solution.

Another solution is to provide a module script like:

<script type="module">
    window.__browserHasModules = true;
</script>

but we know the ECMAScript modules are deferred, so we have to defer all other scripts. Also not good.

What we need is a standardized way to inform the browser, that some script shouldn’t be executed if ESM is supported.

The nomodule proposal

Domenic raised the discussion, which resulted in the proposal.

There were two main ideas

  • the <nomodule/> tag:
<script type="module" src="app.js"></script>
<nomodule>
  <script defer src="bundle.js"></script>
</nomodule>
  • or nomodule attribute:
<script type="module" src="app.js"></script>
<script nomodule defer src="bundle.js"></script>

As the attribute doesn’t require any additional implementation in the existing browsers, it was decided to stay with it (there were many other reasons).

How to use

The way to use the attribute is described in the following code:

<!--
    Only the browsers WITH ECMAScript modules (aka ES6 modules)
    support will execute the following script
    -->
    <script type="module" src="./ecmascript-modules-main.js"></script>

    <!--
    Only the browsers WITHOUT ES modules support
    will execute the following script.
    Browsers WITH ES modules support will ignore it.
    -->
    <script nomodule src="./bundled-main.js"></script>

where ecmascript-modules-main.js is the entry file in your app, and bundled-main.js is its bundled version.

Here is a simple demo with inline scripts, which you can run in your browser:

<script type="module">
  alert('The browser DOES support ES modules');
</script>
<script nomodule>
  alert('The browser DOES NOT support ES modules');
</script>
Demo

You can also consider the es-modules-utils way, which also adds a couple additions on top of that:

<script
    module="./ecmascript-modules-main.js"
    no-module="./bundled-main.js"

    add-global-class
    add-global-variable

    src="es-modules-utils/no-module-fallback.js"
>
</script>

The utility loads the bundled script or the ECMAScript module, depending on the browser support (nomodule is used under the hood) and provides two additional abilities, covered by the boolean attributes:

  • add-global-class: enables adding the <html class="esmodules"> if ES modules are supported, <html class="no-esmodules"> otherwise (can be used e.g. to show some animation till the ES modules graph is loaded and executed)
  • add-global-variable: which enables adding the global Boolean variable window.esmodules=true/false (can be used e.g. to decide which method to use to include new scripts inside the JavaScript)

For example, if your module graph is quite extensive and is loaded for the first time, it may take some time until the files and graph are loaded and executed. To show the loader and hide it after the JS is executed you can use es-modules-utils and the following approach:

<style>
    .no-esmodules .loader,
    .esmodules .loader {
        display: none;
    }
</style>

<img class="loader" src="./loader.gif">
Demo

The current state of support

The nomodule attribute is already added to the HTML standard and described in the specification with the examples.

Also thanks to WebKit, nomodules tests are added to the web-platform-tests, which give browser projects confidence that they are shipping software that is compatible with other implementations

The good news is, that the attribute is “supported” without any additions in all browsers without ES Modules support, as they just execute the scripts without doing anything with the unknown attributes.

Currently nomodule has been implemented in Safari Technology preview, but as the proposal came from Google initially, there is no doubt it will be delivered together with ES modules support in Chrome (the issue).

In addition, it’s just been confirmed and should arrive soon in Microsoft EDGE. Firefox also has an issue for this.

Conclusions

The idea is that when any browser ships native ES Module support in their stable version, they will also support the nomodule attribute (according to the spec).

This gives us the ability to include two versions of our scripts in HTML:

  • classic - bundled with bundlers/transpilers
  • the original ES Modules

As browsers start supporting the ECMAScript modules, all the functionality will automatically switch to use the ESM scripts.

Provide your code in
        <pre><code class="{language}"></code></pre>
        
tags