- ECMAScript modules in browsers
- «Bare» import specifiers aren’t currently supported
- nomodule for backwards compatibility
- Browser issues
- Defer by default
- Inline scripts are also deferred
- Async works on external & inline modules
- Browser issues
- Modules only execute once
- Browser issues
- Always CORS
- Browser issues
- Credentials by default
- Mime-types
- Browser issues
- Performance recommendations, dynamic import & more!
- Elsewhere
- Contact
ECMAScript modules in browsers
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.mjs'; import foo> from '/utils/bar.mjs'; import foo> from './bar.mjs'; import foo> from '../bar.mjs'; // Not supported: import foo> from 'bar.mjs'; import foo> from 'utils/bar.mjs';
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.mjs"> 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) . Fixed in Firefox nightly!
- Edge doesn’t support nomodule (issue) . Fixed in Edge 16!
- Safari 10.1 doesn’t support nomodule . Fixed in Safari 11! For 10.1, there’s a pretty smart workaround.
Defer by default
script type="module" src="1.mjs"> script> script src="2.js"> script> script defer src="3.js"> script>
Live demo. The order should be 2.js , 1.mjs , 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
script type="module"> addTextToBody("Inline module executed"); script> script src="1.js"> script> script defer> addTextToBody("Inline script executed"); 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
script async type="module"> import addTextToBody> from './utils.mjs'; addTextToBody('Inline module executed.'); script> script async type="module" src="1.mjs"> 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
Modules only execute once
script type="module" src="1.mjs"> script> script type="module" src="1.mjs"> script> script type="module"> import "./1.mjs"; script> 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
Always CORS
script type="module" src="https://….now.sh/no-cors"> script> script type="module"> import 'https://….now.sh/no-cors'; addTextToBody("This will not execute."); script> 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) . Fixed in Firefox 59!
- Edge loads module scripts without CORS headers (issue) . Fixed in Edge 16!
Credentials by default
Most CORS-based APIs will send credentials (cookies etc) if the request is to the same origin, but for a while fetch() and module scripts were exceptions. However, that all changed, and now fetch() and module scripts behave the same as other CORS-based APIs.
However, that means you’ll encounter three exciting varieties of browser support:
- Old versions of browsers that, against the spec at the time, sent credentials by to same-origin URLs by default.
- Browsers that followed the spec at the time, and did not send credentials to same-origin URLs by default.
- New browsers that follow the new spec, and send credentials to same-origin URLs by default.
If you hit this issue, you can add the crossorigin attribute, which will add credentials to same-origin requests, but not cross-origin request, in any browser that follows the old spec. It doesn’t do anything if the browser follows the new spec, so it’s safe to use.
script src="1.js"> script> script type="module" src="1.mjs"> script> script type="module" crossorigin src="1.mjs"> script> script type="module" crossorigin src="https://other-origin/1.mjs"> script> script type="module" crossorigin="use-credentials" src="https://other-origin/1.mjs"> script>
Mime-types
Unlike regular scripts, modules scripts must be served with one of the valid JavaScript MIME types else they won’t execute. The HTML Standard recommends text/javascript .
Browser issues
And that’s what I’ve learned so far. Needless to say I’m really excited about ES modules landing in browsers!
Performance recommendations, dynamic import & more!
Check out the article on Web Fundamentals for a deep-dive into module usage.
Hello, I’m Jake and that is my tired face. I’m a developer of sorts.
Elsewhere
Contact
Feel free to throw me an email, unless you’re a recruiter, or someone trying to offer me ‘sponsored content’ for this site, in which case write your request on a piece of paper, and fling it out the window.