ECMAScript modules in browsers
ES modules are starting to land in browsers! They're in…
- Safari 10.1.
- Chrome Canary 60 – behind the Experimental Web Platform flag in
chrome:flags
. - Firefox 54 – behind the
dom.moduleScripts.enabled
setting inabout:config
. - Edge 15 – behind the Experimental JavaScript Features setting in
about:flags
.
<script type="module"> import {addTextToBody} from './utils.js'; addTextToBody('Modules are pretty cool.'); </script>
// utils.js export function addTextToBody(text) { const div = document.createElement('div'); div.textContent = text; document.body.appendChild(div); }
All you need is type=module
on the script element, and the browser will treat the inline or external script as an ECMAScript module.
There are already some great articles on modules, but I wanted to share a few browser-specific things I'd learned while testing & reading the spec:
"Bare" import specifiers aren't currently supported
// Supported: import {foo} from 'https://jakearchibald.com/utils/bar.js'; import {foo} from '/utils/bar.js'; import {foo} from './bar.js'; import {foo} from '../bar.js'; // Not supported: import {foo} from 'bar.js'; import {foo} from 'utils/bar.js';
Valid module specifiers must match one of the following:
- A full non-relative URL. As in, it doesn't throw an error when put through
new URL(moduleSpecifier)
. - Starts with
/
. - Starts with
./
. - Starts with
../
.
Other specifiers are reserved for future-use, such as importing built-in modules.
nomodule for backwards compatibility
<script type="module" src="module.js"></script> <script nomodule src="fallback.js"></script>
Browsers that understand type=module
should ignore scripts with a nomodule
attribute. This means you can serve a module tree to module-supporting browsers while providing a fall-back to other browsers.
Browser issues
- Firefox doesn't support
nomodule
(issue). - Edge doesn't support
nomodule
(issue). - Safari 10.1 doesn't support
nomodule
, but it's fixed in their latest technical preview.
Defer by default
<!-- This script will execute after… --> <script type="module" src="1.js"></script> <!-- …this script… --> <script src="2.js"></script> <!-- …but before this script. --> <script defer src="3.js"></script>
Live demo. The order should be 2.js
, 1.js
, 3.js
.
The way scripts block the HTML parser during fetching is baaaad. With regular scripts you can use defer
to prevent blocking, which also delays script execution until the document has finished parsing, and maintains execution order with other deferred scripts. Module scripts behave like defer
by default – there's no way to make a module script block the HTML parser while it fetches.
Module scripts use the same execution queue as regular scripts using defer
.
Inline scripts are also deferred
<!-- This script will execute after… --> <script type="module"> addTextToBody("Inline module executed"); </script> <!-- …this script… --> <script src="1.js"></script> <!-- …and this script… --> <script defer> addTextToBody("Inline script executed"); </script> <!-- …but before this script. --> <script defer src="2.js"></script>
Live demo. The order should be 1.js
, inline script, inline module, 2.js
.
Regular inline scripts ignore defer
whereas inline module scripts are always deferred, whether they import anything or not.
Async works on external & inline modules
<!-- This executes as soon as its imports have fetched --> <script async type="module"> import {addTextToBody} from './utils.js'; addTextToBody('Inline module executed.'); </script> <!-- This executes as soon as it & its imports have fetched --> <script async type="module" src="1.js"></script>
Live demo. The fast-downloading scripts should execute before the slow ones.
As with regular scripts, async
causes the script to download without blocking the HTML parser and executes as soon as possible. Unlike regular scripts, async
also works on inline modules.
As always with async
, scripts may not execute in the order they appear in the DOM.
Browser issues
- Firefox doesn't support
async
on inline module scripts (issue).
Modules only execute once
<!-- 1.js only executes once --> <script type="module" src="1.js"></script> <script type="module" src="1.js"></script> <script type="module"> import "./1.js"; </script> <!-- Whereas normal scripts execute multiple times --> <script src="2.js"></script> <script src="2.js"></script>
If you understand ES modules, you'll know you can import them multiple times but they'll only execute once. Well, the same applies to script modules in HTML – a module script of a particular URL will only execute once per page.
Browser issues
- Edge executes modules multiple times (issue).
Always CORS
<!-- This will not execute, as it fails a CORS check --> <script type="module" src="https://….now.sh/no-cors"></script> <!-- This will not execute, as one of its imports fails a CORS check --> <script type="module"> import 'https://….now.sh/no-cors'; addTextToBody("This will not execute."); </script> <!-- This will execute as it passes CORS checks --> <script type="module" src="https://….now.sh/cors"></script>
Unlike regular scripts, module scripts (and their imports) are fetched with CORS. This means cross-origin module scripts must return valid CORS headers such as Access-Control-Allow-Origin: *
.
Browser issues
- Firefox fails to load the demo page (issue).
- Edge loads module scripts without CORS headers (issue).
No credentials
<!-- Fetched with credentials (cookies etc) --> <script src="1.js"></script> <!-- Fetched without credentials --> <script type="module" src="1.js"></script> <!-- Fetched with credentials --> <script type="module" crossorigin src="1.js?"></script> <!-- Fetched without credentials --> <script type="module" crossorigin src="https://other-origin/1.js"></script> <!-- Fetched with credentials--> <script type="module" crossorigin="use-credentials" src="https://other-origin/1.js?"></script>
Most CORS-based APIs will send credentials (cookies etc) if the request is to the same origin, but fetch()
and module scripts are exceptions – they don't send credentials unless you ask for them.
You can add credentials to a same-origin module by including the crossorigin
attribute (which seems a bit weird to me, and I've questioned this in the spec). If you want to send credentials to other origins too, use crossorigin="use-credentials"
. Note that the other origin will have to respond with the Access-Control-Allow-Credentials: true
header.
Also, there's a gotcha related to the "Modules only execute once" rule. Modules are keyed by their URL, so if you request a module without credentials, then request it with credentials, you'll get the same without-credentials module back. This is why I've used a ?
in the URLs above, to make them unique.
Browser issues
- Chrome requests same-origin modules with credentials (issue).
- Safari requests same-origin modules without credentials even if you use the
crossorigin
attribute (issue). - Edge gets this backwards. It sends credentials to the same origin by default, but then doesn't send them if you add the
crossorigin
attribute (issue).
Firefox is the only one that gets this right – well done folks!
Mime-types
Unlike regular scripts, modules scripts must be served with one of the valid JavaScript MIME types else they won't execute.
Browser issues
- Edge executes scripts with invalid MIME types (issue).
And that's what I've learned so far. Needless to say I'm really excited about ES modules landing in browsers!