Order of DOM Execution Issue
Asked Answered
I

4

7

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?

Iams answered 10/1, 2014 at 20:47 Comment(4)
What do you mean by "before all JS has been loaded in"? You cannot really control that when dynacimally generatig script elements that will load asynchronically. And since hanging scripts always lead to bad UX, you should avoid those instead of including them in your requirements.Cumbrance
What problem are you trying to solve? Maybe include an example of where you need this behaviour.Cumbrance
@Cumbrance Hanging scripts are out of my control because these are not web pages that I controlIams
@Cumbrance And I don't need to have my JS run before ANY other JS has been loaded in and executed. I just need to make sure it doesn't get hung up by a hanging script on the page (because that will delay when the DOMContentLoadedevent fires).Iams
E
1

...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).

Easterly answered 13/1, 2014 at 4:12 Comment(17)
Thanks for the detailed answer. Unfortunately in some cases the JavaScript will not be able to be pasted in on the bottom of the page. Do you have an answer with this requirement in place?Iams
@JustinMeltzer: You need to have enough control over markup to move the tags (if you don't , you have bigger problems than sloppy performance :) Yes, if you are behind F5 you could probably apply a custom stream profile to do so on the fly, and there is likely an existing Apache module that does the same - but markup isn't supposed to require such sick workarounds. What are you on, RoR? If so, this guy has the right idea. If not, you should update the question with your stack details/limitations.Easterly
My company provides a service that requires our clients to paste a snippet of code into their pages, which is why we don't have immediate control over it.Iams
@JustinMeltzer: aha, so you are the third party with questionable code :) No chance of having the clients figuring out how to paste code at the bottom of the page? I'll update the answer later today, you might have luck deferring to a different load-like event.Easterly
Yup! At this point, no - It would take way to much work to get all of our clients to switch over.Iams
Thanks for the update! Will the setTimeout be triggered even before a hanging script tag is loaded in?Iams
I want to avoid the case where there is a script that is taking forever to load, which causes my JavaScript to start executing at a very late point.Iams
The only thing I wanted to add is I wrote this before @ov and I actually use this idea in production since I created kitgui.com which is an injectable cloud cms in any tech. I didn't get this idea directly from anyone else so not sure why I don't get a nod for that (oc edited 3 hours ago while I edited 23 hours ago, ahem cough cough).Lepido
@JustinMeltzer: hold on, so you want the target.js to execute asap, rather than not interfere with the existing javascript on the page? If you really want to prioritise your javascript over that of your clients, you just have them load it with a "normal" synchronous script tag. Except in this case you'll end up being the evil third party that slows down the rest of the page. You should update your question anyway; as it stands right now you're just asking for someone to say "yes, it is safe to start parsing the DOM right away" and reap the bounty, not troubleshoot your specific case.Easterly
@JasonSebring: ouch, I'm pretty sure you were suggesting to use setTimeout as an alternative to a DOM ready callback, while the solution is to defer the loader creation - not something explored in your post or other sources online I went through while refreshing my memory on the mechanics of setTimeout. Using comments like // won't fart in your code sounds like giving up to me, but I'm sure you'll get your fair share of upvotes :) Not sure what all the commotion is about anyway since it seems deferral doesn't fit OP's expectations anyway.Easterly
@o.v. no no, I don't want my JS to run right away. I want it to run after the DOM has been parsed because my JavaScript needs to be able to manipulate random nodes on the page. However I don't want my script to take forever to start executing if another script that I don't control is hanging and taking forever to load. Does that make sense?Iams
ok whatever, you are probably right. just nice to get a frickin up vote :)Lepido
ok last comment, "my company requires snippets of code to be placed..." that's what kitgui does. BUT I use setTimeout that calls itself again and again with a set stopping iteration checking for dependant scripts. This is a bit hackish though. I'm done with this one. Tiresome.Lepido
@JustinMeltzer: sorry, this time it's my bad (I say "asap", I mean "as soon as DOM is ready and I don't care about any other scripts on the page, I want to be first in line"). The problem is that scripts are executed in a linear fashion, so you have to pre-determine their desired order of execution ahead of time (gets tricker with async scripts though). You can't say "run target.js after all client js is done... unless it's really slow". Happy to take this to the chat for a few minutes if that would clear things up.Easterly
sure, how do I move this to chat?Iams
@JasonSebring: that's ok, older jQuery used return setTimeout( jQuery.ready, 13 );, if you want to see "a bit hackish", you go through ticket #6781 and then the source file :)Easterly
@JustinMeltzer: with the length of the comment thread on this, I expected to be prompted by now :) chat.stackoverflow.com/rooms/17/javascript is the public roomEasterly
L
1

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/

Lepido answered 12/1, 2014 at 22:47 Comment(5)
Why do you think a setTimeout will defer your function until the DOM is loaded?Cumbrance
Thanks for making me double check those examples, @Bergi. Loading external synchronous scripts won't work with dependent setTimeout code, however, inline immediate works as I've demonstrated here.Lepido
@JasonSebring: hmm, I remember having used a 0ms setTimeout for script execution deferral in a similar situation, but I can't quite recall where the solution came from. Any references?Easterly
John Resig mentioned something like this in a blog post ejohn.org/blog/how-javascript-timers-work but not specifically for this approach. I did not know JavaScript was single threaded until I read that post :) which helped this idea. All the script must execute first in its entirety on the page that is "in band", meaning in the current context. setTimeout code will be deferred till after the immediate script runs (and page dom being parsed as well).Lepido
@JasonSebring: +1 for the sourceEasterly
E
1

...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).

Easterly answered 13/1, 2014 at 4:12 Comment(17)
Thanks for the detailed answer. Unfortunately in some cases the JavaScript will not be able to be pasted in on the bottom of the page. Do you have an answer with this requirement in place?Iams
@JustinMeltzer: You need to have enough control over markup to move the tags (if you don't , you have bigger problems than sloppy performance :) Yes, if you are behind F5 you could probably apply a custom stream profile to do so on the fly, and there is likely an existing Apache module that does the same - but markup isn't supposed to require such sick workarounds. What are you on, RoR? If so, this guy has the right idea. If not, you should update the question with your stack details/limitations.Easterly
My company provides a service that requires our clients to paste a snippet of code into their pages, which is why we don't have immediate control over it.Iams
@JustinMeltzer: aha, so you are the third party with questionable code :) No chance of having the clients figuring out how to paste code at the bottom of the page? I'll update the answer later today, you might have luck deferring to a different load-like event.Easterly
Yup! At this point, no - It would take way to much work to get all of our clients to switch over.Iams
Thanks for the update! Will the setTimeout be triggered even before a hanging script tag is loaded in?Iams
I want to avoid the case where there is a script that is taking forever to load, which causes my JavaScript to start executing at a very late point.Iams
The only thing I wanted to add is I wrote this before @ov and I actually use this idea in production since I created kitgui.com which is an injectable cloud cms in any tech. I didn't get this idea directly from anyone else so not sure why I don't get a nod for that (oc edited 3 hours ago while I edited 23 hours ago, ahem cough cough).Lepido
@JustinMeltzer: hold on, so you want the target.js to execute asap, rather than not interfere with the existing javascript on the page? If you really want to prioritise your javascript over that of your clients, you just have them load it with a "normal" synchronous script tag. Except in this case you'll end up being the evil third party that slows down the rest of the page. You should update your question anyway; as it stands right now you're just asking for someone to say "yes, it is safe to start parsing the DOM right away" and reap the bounty, not troubleshoot your specific case.Easterly
@JasonSebring: ouch, I'm pretty sure you were suggesting to use setTimeout as an alternative to a DOM ready callback, while the solution is to defer the loader creation - not something explored in your post or other sources online I went through while refreshing my memory on the mechanics of setTimeout. Using comments like // won't fart in your code sounds like giving up to me, but I'm sure you'll get your fair share of upvotes :) Not sure what all the commotion is about anyway since it seems deferral doesn't fit OP's expectations anyway.Easterly
@o.v. no no, I don't want my JS to run right away. I want it to run after the DOM has been parsed because my JavaScript needs to be able to manipulate random nodes on the page. However I don't want my script to take forever to start executing if another script that I don't control is hanging and taking forever to load. Does that make sense?Iams
ok whatever, you are probably right. just nice to get a frickin up vote :)Lepido
ok last comment, "my company requires snippets of code to be placed..." that's what kitgui does. BUT I use setTimeout that calls itself again and again with a set stopping iteration checking for dependant scripts. This is a bit hackish though. I'm done with this one. Tiresome.Lepido
@JustinMeltzer: sorry, this time it's my bad (I say "asap", I mean "as soon as DOM is ready and I don't care about any other scripts on the page, I want to be first in line"). The problem is that scripts are executed in a linear fashion, so you have to pre-determine their desired order of execution ahead of time (gets tricker with async scripts though). You can't say "run target.js after all client js is done... unless it's really slow". Happy to take this to the chat for a few minutes if that would clear things up.Easterly
sure, how do I move this to chat?Iams
@JasonSebring: that's ok, older jQuery used return setTimeout( jQuery.ready, 13 );, if you want to see "a bit hackish", you go through ticket #6781 and then the source file :)Easterly
@JustinMeltzer: with the length of the comment thread on this, I expected to be prompted by now :) chat.stackoverflow.com/rooms/17/javascript is the public roomEasterly
C
0

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.

Collegium answered 13/1, 2014 at 0:4 Comment(0)
O
0

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/

Overtrump answered 13/1, 2014 at 5:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.