With the addition of ES modules, there's now no fewer than 24 ways to load your JS code: (inline|not inline) x (defer|no defer) x (async|no async) x (type=text/javascript | type=module | nomodule) -- and each of them is subtly different.
This document is a comparison of various ways the <script>
tags in HTML are processed depending on the attributes set.
If you ever wondered when to use inline <script async type="module">
and when <script nomodule defer src="...">
, you're in the good place!
Note that this article is about <script>
s inserted in the HTML; the behavior of <script>
s inserted at runtime is slightly different - see Deep dive into the murky waters of script loading by Jake Archibald (2013)
Image replaces 1000 words, so here it is, for the good start: (credit: https://developers.google.com/web/fundamentals/primers/modules#module-vs-script)
As you can see in the picture, async
in particular might be a bit tricky: with legacy scripts, you generally use it to make things happen later, whereas with modules, you generally use it to make things happen earlier (this is because module scripts are deferred by default).
defer
is older, async
is just slightly newer. Both are well supported.
The intuitive difference between async
and defer
is that async
tend to execute earlier (they don't have to wait for HTML to be parsed and DOM to be constructed, and for other scripts to be fetched).
-
"standard" non-module
<script>
(impliedtype=text/javascript
, noasync
, nodefer
)- ❌ blocks the HTML parser
- ℹ️ immediately fetched, parsed and executed, before any following
<script>
s in the HTML - ✔️ in-order execution guaranteed
- ❌ blocks
DOMContentLoaded
event - ❌ given the above, not suitable for non-critical code, as it can lead to "single point of failure" rendering bottleneck and to delayed startup of the dynamic web apps
-
defer
scripts:- ℹ️ for inline (non-module) scripts,
defer
is ignored and has no effect (they are executed immediately)- 💡 if you really, really need it, you can use the base64 hack
- ℹ️ for inline (module) scripts,
defer
is implied (automatic) - ✔️ downloaded without blocking HTML parser
- ✔️ relative order between multiple
defer
scripts execution is guaranteed (if they all havesrc
)⚠️ however, this is buggy in IE9-
- ℹ️ executed after DOM is parsed (but just before raising
DOMContentLoaded
) - ❌ blocks
DOMContentLoaded
event (unless the script isasync defer
)
- ℹ️ for inline (non-module) scripts,
-
async
scripts:- ℹ️ for inline (non-module) scripts,
async
is ignored and has no effect - ℹ️ for inline (module) scripts,
async
is supported source - to allow out-of-order, as-soon-as-possible execution - ✔️ downloaded without blocking HTML parser
⚠️ out of order execution; executed as soon as available⚠️ relative order betweenasync
scripts execution is not guaranteed (also applies toasync, type=module
scripts)⚠️ doesn't wait for HTML parsing to finish; may interrupt DOM building (particularly when it gets served from browser's cache)⚠️ blocksload
event (but notDOMContentLoaded
event)⚠️ not supported in IE9-
- ℹ️ for inline (non-module) scripts,
-
async defer
scripts:- interpreted as
async
; falling back todefer
for ancient browser engines that don't supportasync
(IE9...)
- interpreted as
-
type=module
scripts - differences wrttype=text/javascript
scripts:- ℹ️ implies
defer
- ℹ️ for inline scripts, also implies
defer
(contrary to non-module scripts!) - ✔️ hence, guaranteed relative order of execution for all non-
async
module scripts (both inline andsrc
) - ✔️ executes only once, even if script with same
src
is loaded multiple times - ℹ️ may use
import
to declare a dependency on another module script (that's one of the reasons why modules are deferred) - ℹ️ subject to CORS check (cross-origin modules need
Access-Control-Allow-Origin: *
) - ✔️ not executed by the browsers who don't support it
⚠️ however they are still fetched apparently by IE11, Firefox 52 ESR etc.
- ℹ️ implies
-
<script nomodule>
- ✔️ not fetched and not executed by browsers supporting
<script type="module">
⚠️ however some modern browsers are buggy and still fetch it (Safari 10.3 apparently - but there's a workaround)
- ✔️ not fetched and not executed by browsers supporting
-
inline scripts (without
src
):- ℹ️ non-module inline scripts:
async
anddefer
are both ignored; script is blocking HTML parsers and DOM construction and is executed immediately - ℹ️ module inline scripts: implied
defer
; supportingasync
- ❌ not cacheable by the browser
- ℹ️ non-module inline scripts:
-
src
scripts:- ✔️ cacheable by the browser (given proper response headers), hence can be reused on future navigations without fetching from network
type | example use case |
---|---|
script src | a legacy library that is needed by subsequent inline scripts |
script src defer | deferred execution, maintaining order; e.g. a lib that is needed by other defer scripts; progressive enhancement code |
script src async | deferred execution, without order (independent scripts); e.g. self-instantiating analytics lib |
script src async defer | like above, but with IE9 support |
script inline | 1) small piece of code that must be executed immediately, before some subsequent code (inlined polyfills, timers, server-generated configuration), or to register certain event listeners as soon as possible; 2) non-cacheable (generated, often changing etc.) code; 3) experience critical code that is small and the round-trip latency to download it separately would be too much |
script src module | library/app, for modern browsers only |
script src module async | progressive enhancement code, for modern browsers only |
script inline module | small piece of and/or non-cacheable code, for modern browsers only; perhaps inline config that is necessary for another non-async module declared after it |
script inline module async | small piece of and/or non-cacheable progressive enhancement code (independent script), for modern browsers only; it may import s a well-cacheable library |
script nomodule ... | a fallback script for legacy browsers, when shipping ES modules to modern browsers |
script src async defer module nomodule | left as an exercise for the reader |
- https://addyosmani.com/blog/script-priorities/
- https://hacks.mozilla.org/2017/09/building-the-dom-faster-speculative-parsing-async-defer-and-preload/
- https://jakearchibald.com/2017/es-modules-in-browsers/
- https://developers.google.com/web/fundamentals/primers/modules
- https://gist.github.com/jakub-g/5286483ff5f29e8fdd9f
- https://bitsofco.de/async-vs-defer/
Note that the
defer
property does not reflect the fact thattype="module"
causes the script to be deferred.To test if a script is deferred, you need something like:
This might be totally obvious to some, but I found it surprising, since DOM properties generally reflect effective values, whereas DOM attributes reflect the attribute value in the document - that is, the DOM property will be
false
even though the effective value istrue
.(for example,
document.createElement("script").getAttribute("async")
will benull
- however,script
nodes created at run-time areasync
by default, and thereforedocument.createElement("script").async
will betrue
, reflecting the effective value.)