Order of DOM Execution Issue

334 views Asked by At

I have some JavaScript in the HEAD tag that dynamically inserts an asynchronously loading script tag before the last script on the page (that has been currently parsed). This dynamically included script tag contains JavaScript that needs to parse the DOM after the DOM is available, but before all images AND script tags have been loaded in. It's important that the JavaScript starts executing before all JS has been loaded in, because if there is a hanging script, this would lead to a bad user experience. This means I can't wait for the DOMContentLoaded event to fire. I don't have any flexibility as to where I place the first bit of JavaScript that is dynamically including the script tag.

My question is, is it safe for me to start parsing through the DOM right away, without waiting for the DOMContentLoaded event? If not, is there a way for me to do this without waiting for the DOMContentLoaded event?

4

There are 4 answers

17
Oleg On BEST ANSWER

...JavaScript in the HEAD ... dynamically inserts an asynchronously loading script tag before the last script on the page...

I'm assuming the loader script is inline, meaning that the highlighted bit actually refers to the "current" script element i.e. the loader. This happens since only the html preceding the loader script tag has been parsed and interpreted, so the inserted script tag is actually still in the head and not at the bottom of the page. So the target script is limited to performing DOM operations on preceding elements only, unless you wrap the code into a DOM ready callback... which is what you're trying to avoid in the first place!

Basically you want to load all html so that the page is visible/scannable, start loading images/stylesheets (which occurs in non-blocking threads) and then load any javascript. One approach is to put your target script at the bottom of the page, just pick their order correctly (interactivity first, enhancements second, third party analytics/social media integration/anything else super-heavy last) and adjust for your needs. Technically it still blocks the page load, but there are only scripts left at the bottom of the page anyway (and since they are at the bottom, you would be able to directly manipulate DOM as soon as they're loaded, minus some IE7 quirks).

There is a relevant rant/overview I like to link to that provides decent examples and some timing trivia on use and abuse of DOM ready callbacks, as well as the "other side of the story" on why stellar performance could be of lower value than a sane dependency management framework. The subject of latter is far too broad to be exhausted in one answer, but something like requirejs documentation should give you a fair idea of how the pattern works.

Perhaps another pattern for to consider is building an SPA - single page application which leverages asynchronous access to content chunks rather than the "traditional" navigating between complete pages. The pattern comes with an underestimated but rather significant performance benefit from not having to parse and re-execute shared javascript on every page, which would also address your (valid) concern about third-party js performance. After all, just a good caching policy would do wonders for loading time, but poor javascript code or massive frameworks' execution overhead remains.

Update: Figured this out. With your specific scenario in mind (i.e. no control over markup per se, and wanting to be the last script to execute), you should wrap the insertion of the async script element into DOM into a 0ms setTimeout callback:

setTimeout(function(){

    //the rest is how GA operates
    var targetScript = document.createElement('script');
    targetScript.type = 'text/javascript';
    targetScript.async = true;
    targetScript.src = 'target.js';

    var s = document.getElementsByTagName('script')[0];
    s.parentNode.insertBefore(targetScript, s);

}, 0);

Due to the single-threaded nature of the environment, js setTimeout callback is basically added to a queue for 0ms-delayed execution as soon as the thread is no longer busy (more thorough explanation here). So the browser isn't even aware of the need to load, let alone execute, the target script until after all "higher priority" code has been completed! And since DOM is operational when the script tag is being added, you will not have to check for it explicitly in the target script itself (which is handy for when it's loaded "instantly" from cache).

0
Wolfgang Kuehn On

Yes, you should wait for the user agent to tell you that the DOM is loaded. However, there is more than one way to do so.

There is a difference between onreadystatechange to interactive and DOMContentLoaded.

According http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html the first thing the user agent does, after stopping parsing the document, is setting the document readiness to "interactive".

The user agent does not wait for scripts to be loaded.

When the document readiness changes, the user agent will fire readystatechange at the Document object.

So if the scripts you are worrying about are non-inline, you might hook up with readystatechange.

Talking about cross-browser: This is the spec.

0
Alexander On

I strongly advise you to fully read the following article which delves in detail into mysteries of script loading and actual DOM readiness and when is it safe to do what with what, also taking into account browser disrepancies.

http://www.html5rocks.com/en/tutorials/speed/script-loading/

5
King Friday On

The behavior of the following techniques make it safe to parse DOM ...

  1. Using window load or DomContentLoaded event
  2. Declare or inject your script at the bottom of the page
  3. Place "async" attribute on your script tag
  4. or doing this:
<script>
    setTimeout(function(){
     // script declared inside here will execute after the DOM is parsed
    },0);
</script>

Also, these will NOT BLOCK the page loading in DOM.

There is no need to call the DomContentLoaded event when declaring script below any DOM you are depending on UNLESS you are needing to do size calculations or positioning as images/video will change the sizing of things if width/height is not specified.

Here is some scenarios where this works.

DEPENDENT DOM IS ABOVE
<script src="jquery.js"></script>
<script>
    $('mydom').slideDown('fast');
</script>

or try this:

<script>
    // won't fart
    setTimeout(function(){ console.log(document.getElementById('mydom').innerHTML); },0);
</script>
DEPENDENT DOM IS BELOW or ABOVE (dont' matter)

Here's my little test for you to see setTimeout working as its one of those strange things I didn't notice until recently so its nice to see a working example of it.

http://jsfiddle.net/FFLL2/