Evaluation order of <script> tags
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 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
All of them, with the exception of async scripts, are guaranteed to be executed prior to the
<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
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
They are loaded asynchronously, but is executed synchronously right before the
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> tags created via
document.createElement('script') and then inserted into the DOM.
Unfortunately, dynamically inserted scripts' execution time and order wildly differ between browsers.
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
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.
type="module" for inline script that's dynamically inserted will make them run in insertion order for both Chrome and Safari, but not Firefox.
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
Data uri is the easiest method, as you can simply do
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.
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.