Alex Morales
Alex Morales

Reputation: 2791

"Load" event on script with async and/or defer

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.

Upvotes: 40

Views: 43284

Answers (3)

Raphael Schweikert
Raphael Schweikert

Reputation: 18556

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.

Upvotes: 0

Jason C
Jason C

Reputation: 40315

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.

Upvotes: 7

Daniel Oram
Daniel Oram

Reputation: 8411

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.

Upvotes: 64

Related Questions