Skip to content

Instantly share code, notes, and snippets.

@jakub-g
Last active November 18, 2024 11:31
Show Gist options
  • Save jakub-g/385ee6b41085303a53ad92c7c8afd7a6 to your computer and use it in GitHub Desktop.
Save jakub-g/385ee6b41085303a53ad92c7c8afd7a6 to your computer and use it in GitHub Desktop.
async scripts, defer scripts, module scripts: explainer, comparison, and gotchas

<script> async, defer, async defer, module, nomodule, src, inline - the cheat sheet

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)

Visual representation

Image replaces 1000 words, so here it is, for the good start: (credit: https://developers.google.com/web/fundamentals/primers/modules#module-vs-script) image

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).

Standard vs async vs defer vs async defer

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> (implied type=text/javascript, no async, no defer)

    • 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)
    • ℹ️ 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 have src)
    • ℹ️ executed after DOM is parsed (but just before raising DOMContentLoaded)
    • blocks DOMContentLoaded event (unless the script is async defer)
  • 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 between async scripts execution is not guaranteed (also applies to async, type=module scripts)
    • ⚠️ doesn't wait for HTML parsing to finish; may interrupt DOM building (particularly when it gets served from browser's cache)
    • ⚠️ blocks load event (but not DOMContentLoaded event)
    • ⚠️ not supported in IE9-
  • async defer scripts:

type=module vs non-module (type=text/javascript) vs <script nomodule>

  • type=module scripts - differences wrt type=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 and src)
    • ✔️ 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.
  • <script nomodule>

Inline vs src

  • inline scripts (without src):

    • ℹ️ non-module inline scripts: async and defer are both ignored; script is blocking HTML parsers and DOM construction and is executed immediately
    • ℹ️ module inline scripts: implied defer; supporting async
    • ❌ not cacheable by the browser
  • src scripts:

    • ✔️ cacheable by the browser (given proper response headers), hence can be reused on future navigations without fetching from network

Summary of use cases for various kinds of scripts

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 imports 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

More reading

@GrosSacASac
Copy link

The illustration and description of async is miss-leading.
It loads and executes with lower priority than all the other images, scripts, css. It most likely executes after everything else, after DOMContentLoaded. It was made a standard because the industry used it already in hand-made setTimeout + eval techniques to load ads and decorative features.
The problem with setTimeout is that you have to guess the time it takes for the page to load. If the guess is too long it is not optimal, if it is too low, it will execute before more important scripts... async solves this problem: With async it is possible to execute ads or decorative features as soon as possible, but it has low priority, so the meaning of as soon as possible should not be taken in a stand alone manner.

@GrosSacASac
Copy link

async on script module will not execute earlier than script module without

@GrosSacASac
Copy link

Putting script at the end of the body has the same effect as script defer, but script defer gives the browser opportunity to download and parse earlier

@mindplay-dk
Copy link

mindplay-dk commented Oct 15, 2021

Note that the defer property does not reflect the fact that type="module" causes the script to be deferred.

To test if a script is deferred, you need something like:

function isDeferred(script: HTMLScriptElement) {
  return script.defer || script.type === "module"; // modules are implicitly deferred
}

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 is true.

(for example, document.createElement("script").getAttribute("async") will be null - however, script nodes created at run-time are async by default, and therefore document.createElement("script").async will be true, reflecting the effective value.)

@ramrami
Copy link

ramrami commented Jun 21, 2024

Another important difference is regarding CSS:

  • "standard" non-module <script>: will wait for already-parsed stylesheets to load
  • defer scripts: will wait for all stylesheets to load
  • async scripts: do not wait for stylesheets to load

A common mistake for example is to put a small inline script that handles dark mode in the head after the main CSS, if that CSS file takes 10s to load, then the small script will block everything for at least 10s, moving the script before the CSS solves this.

Great article on the subject here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment