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.