Evaluation order of <script> tags

| permalink | javascript

While developing a suitable HTML Import replacement, I've ran into some problem trying to understand the execution order of script tags, here are my findings.

Parser-inserted scripts

Parser-inserted scripts are those that exists in the page itself, which means either it's embedded into the HTML (possibly generated at server side), or was generated with document.write.

All of them, with the exception of async scripts, are guaranteed to be executed prior to the DOMContentLoaded event.

<script>, both inline and external (via the src attribute)

They are loaded and executed synchronously, in order and in place (parser-blocking).

<script async> or <script async="true">

They are loaded and executed asynchronously, which can occur at anytime.

This means the script is completely non-blocking - parser and script.

Does not affect inline scripts.

Note - because document.write will overwrite the entire page if not executed during the HTML parsing phase, you do not want it inside any asynchronous scripts because it can fire after parser is finished.

<script defer> or <script defer="true">

They are loaded asynchronously, but is executed synchronously right before the DOMContentLoaded event.

This means the script is non-parser blocking, but does block other scripts during the execution phase.

Does not affect inline scripts.

<script type="module">, inline and external

They are automatically deferred, they also face CORS (Cross Origin Resource Sharing) restrictions for loading external resources.

It applies to inline script, so you can defer inline script this way.

Script-inserted scripts

Those are <script> tags created via document.createElement('script') and then inserted into the DOM.

Unfortunately, dynamically inserted scripts' execution time and order wildly differ between browsers.

Browser commonality

With async set to false, all browsers will execute dynamically inserted scripts in their insertion order.

Defer attribute appears to have no effect, this means we can't ensure script-inserted script is executed before DOMContentLoaded, even if we inserted the script before DOMContentLoaded triggers. This makes sense because script insertion can occur after DOMContentLoaded.

Browser difference

It appears that Firefox alone will treat inserted script as parser-blocking when inserted before DOMContentLoaded is fired, while both Safari and Chrome will not.

This appears to be an implementation difference.

Even though the specification states that script-inserted scripts should obey insertion order (as long as async is set to false), inline scripts are always executed first, in order.

Setting type="module" for inline script that's dynamically inserted will make them run in insertion order for both Chrome and Safari, but not Firefox.

document.createElement('script') has async set to true by default, to override this you need to set it to false.

The reason for this is that all script elements are non-blocking (async) by default, the HTML parser unset this when it goes through the parser-inserted script elements.

How to run inserted inline scripts in order?

My first instinct is to use type="module", however due to Firefox's misbehaviour I can't rely on it.

The next solutions is to hack the src attribute to accept JavaScript, either using data uri or blob object.

Data uri is the easiest method, as you can simply do src="data:text/javascript,alert('triggered')".

This is nice and simple, but you might run into implementation issues for sites that requires hardened security via CSP (Content Security Policy) that locks down data uri and blobs.

Finally you can do it the "proper" JavaScript way which is wait until the external script has finished loading then trigger the onload event to insert the inline script.

The problem here is performance as by inserting all scripts at the same time allows the browser to download them in parallel while waiting for onload delays that. It is also a more complicated way of loading scripts.

At the end of the day, data uri is likely the best method while we wait for Firefox to align its behaviour for type="module" to that of the other two browsers.

Update: found another person also did a bunch of research on this topic, will read further some day.