38

When embedding scripts like:

<script src="..." async defer></script>

Is there a way to know when they're finished loading?

Usually when the window.load event is called, one would expect all scripts to be ready as well. But I don't know if that still holds when you load them with async or defer. I've read some docs online but couldn't find anything conclusive on this issue.

3 Answers 3

59

Answer:
You could take advantage of the onload event attribute in order to perform some kind of callback once your script is loaded.

Example:
In the example html script element below when the script (jquery library from google api) finishes loading asynchronously, an alert will pop up saying 'resource loaded'.

<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js" async defer onload="alert('resource loaded');">


Note: The src script will load very fast because it is hosted by google so the pop up will most likely appear as soon as the page/DOM has loaded.








Edit: added important information originally from comment.

window.onload waits for everything to load before firing whereas document.onload fires when the Document Object Model (DOM) is ready.

So if you've got async scripts document.onload will execute first while window.onload will wait for those asynchronous scripts to finish loading.

To summarize:

  • window.onload will take async scripts into account.
  • document.onload will not take async scripts into account.
3
  • ok so the window.onload event doesnt take async scripts into account right? Commented Apr 12, 2017 at 12:51
  • 11
    window.onload waits for everything to load before firing whereas document.onload fires when the DOM is ready. So if you've got async scripts document.onload will execute first while window.onload will wait. So window.onload will take asyncscripts into account Commented Apr 13, 2017 at 8:00
  • Can you please share a document or blog where it can be followed further? Commented Oct 20, 2021 at 18:13
5

Emphasis mine:

Is there a way to know when they're finished loading?

Usually when the window.load event is called, one would expect all scripts to be ready as well. But I don't know if that still holds when you load them with async or defer. I've read some docs online but couldn't find anything conclusive on this issue.

Addressing the points in bold (for specific single scripts you can use their onload events), the TL;DR is:

  • The document.DOMContentLoaded event will happen after all normal and deferred scripts load and execute, but doesn't care about async scripts.
  • The window.load event will happen after all normal, async, and deferred scripts load and execute.
  • Note: A script that has both async and deferred set will act as deferred on legacy browsers that don't support async, and will act as async otherwise. So the safe bet is to think of them as async.

The HTML specification does say this, albeit indirectly. The spec defines three distinct script collections that every document has (I'm naming them S1, S2, and S3):

Each Document has a set of scripts that will execute as soon as possible, which is a set of script elements, initially empty. [S1]

Each Document has a list of scripts that will execute in order as soon as possible, which is a list of script elements, initially empty. [S2]

Each Document has a list of scripts that will execute when the document has finished parsing, which is a list of script elements, initially empty. [S3]

Just above that, in the section about preparing script elements, it details how scripts are distributed to those collections. Generally speaking, during load:

  • These are placed in S1 (see step 31.2.2):
    • All async external (scripts with a src) scripts.
    • All async module (depends on type attribute) scripts.
  • These are placed in S2 (defer is irrelevant for these) (see step 31.3.2):
    • Non-async, injected (e.g. by the browser) external scripts.
    • Non-async, injected module scripts.
  • These are placed in S3 (see step 31.4):
    • Deferred, non-async, non-injected external scripts.
    • All non-async, non-injected module scripts (defer is irrelevant for these).
  • These are executed synchronously and aren't placed in any of the collections:
    • Non-deferred, non-async, non-injected external scripts (see step 31.5).
    • All inline (without a src) scripts (neither async nor defer apply to these) (see step 32).

In simplified terms:

  • S1 contains all the async external/module scripts.
  • S2 contains all the non-async injected external/module scripts.
  • S3 contains all the deferred external/module scripts.
  • Inline scripts and vanilla external scripts are executed as they're loaded and parsed (as part of the parsing operation).

The HTML spec then goes on to define what happens after parsing is complete, where the relevant parts are, in order:

  • Change document's ready state to "interactive"; fires document.readystatechange (see step 3)
  • Execute all scripts in S3 (deferred non-async non-injected) (see step 5)
  • Queued (will happen >= now): Fire a DOMContentLoaded event on document (see step 6.2)
  • Wait until all scripts in S1 (async) and S2 (non-async injected) have been executed (see step 7)
  • Wait until any other load-blocking operations have been completed (see step 8)
  • Queued:
    • Change document's ready state to "complete"; fires document.readystatechange (see step 9.1)
    • Fire a load event on window (see step 9.5)
    • Fire a pageshow event on window (see step 9.11)
    • If the document is in some container (e.g. an iframe), fire a load event on the container (see link in step 9.12)

In simplified terms, the events that depend on script executions are:

  • document.DOMContentLoaded happens after all the deferred scripts are executed.
  • document.readystatechange ("complete") and window.load happen after all scripts are executed.
  • window.pageshow also happens after all scripts are executed, although it happens at other times later, too.
  • If there's a container like an iframe or something, its load event happens after all scripts are executed as well.

Btw, as for scripts with both async and defer set, the part describing these attributes says:

The defer attribute may be specified even if the async attribute is specified, to cause legacy web browsers that only support defer (and not async) to fall back to the defer behavior instead of the blocking behavior that is the default.

For "modern" browsers, I assume the behavior when both are specified is to just adhere to the logic above, i.e. those scripts end up in S1 and defer is essentially ignored.

So uh... yup.

0

My workaround for the lack of readyState on async scripts only works when targeting browsers with support for ESModules:

Change the <script> tags from

<script src="/path/to/script" async></script>

to

<script>(window.asyncScripts = window.asyncScripts || []).push(import("/path/to/script"));</script>

then, to await their load, do:

Promise.all(window.asyncScripts).then(() => {
  // Your code here
});

You can also use await Promise.all(window.asyncScripts) if inside an async context.

This should have the same semantics as the <script async> tag because the import() function does an async load by default, but it may delay loading of the script by a tiny amount because the browser needs to switch from the parser to the JS executor.

If this is an issue, a <link rel="modulepreload" href="/path/to/script" /> will help.

5
  • 1
    What "lack of load event on async script" exactly? Listening to the window's load event will wait until all the parsed <script> have loaded and executed, including async and type="module" ones, and they also have their own load event you can listen to.
    – Kaiido
    Commented Apr 17 at 9:05
  • load will only fire when all images and other assets have also loaded, which may be much, much later than the scripts. Commented Apr 17 at 9:25
  • And yes, you’re right that <script>s have a load event to listen to but no way to detect whether it has already fired by the time the listener is attached (which would make it never fire at all). In short, <script> tags lack a readyState. Commented Apr 17 at 9:27
  • But just like you're editing the markup to include your "workaround", you could just use the onload attribute.
    – Kaiido
    Commented Apr 17 at 10:44
  • I try to capture two pieces of information: 1. that an async script has loaded and 2. that there was such a script in the first place. With onload I get №1 easily but have to jump through hoops to get №2. With my workaround, I get both in one go. Commented Apr 17 at 11:00

Not the answer you're looking for? Browse other questions tagged or ask your own question.