#1079 enhancement
Sergiu Dumitriu

The dom:loaded event is fired before defered scripts are executed

Reported by Sergiu Dumitriu | June 21st, 2010 @ 10:53 AM | in 2.0

A good performance enhancer is to use the defer attribute on scripts, which allows them to be downloaded in parallel with the main document, and allow faster loading (display) of the content. While prototype itself doesn't really support being defered, other scripts can be. A good practice is to initialize scripts on dom:loaded, which should trigger after all the scripts have been downloaded, parsed and executed.

While the doScroll trick introduced by issue #127 does trigger after the DOM has been completely loaded, it doesn't respect the script execution condition, meaning that it fires while most defered scripts are still in the loading phase, which means that the dom:loaded event will be triggered before most scripts have a chance to register themselves.

Is the doScroll check really helpful? What does it do better than the readystatechange listener? I'm not aware of any major advantages, but there's one really big disadvantage: it prevents good performance by forcing scripts to be serialized.

Comments and changes to this ticket

  • John-David Dalton

    John-David Dalton June 21st, 2010 @ 04:22 PM

    The onreadystatechange will fire complete just before window.onload fires so that is no good.
    doScroll is useful for IE because it will throw errors (assuming it's in the primary document and not an iframe) when called until the DOM is ready (which means before images are loaded and in this case before your deferred scripts).

    I have passed this along to Diego Perini (the dev who researched/discovered the doScroll technique).

    In the meantime have you checked out script loaders like LABjs ?

  • Diego Perini

    Diego Perini June 21st, 2010 @ 04:55 PM

    The "DOMContentLoaded" event and its IE counterpart emulation ONLY depends on the DOM being complete as per the HTML5 specifications describing it. No dependencies exists about stylesheets being loaded/applied or scripts being loaded/executed or any other external binary requirement being available (img/object/applet etc.).

    So if you use the "defer" attribute to load and execute script asynchronously you should also setup notification handlers for when they are ready so you know when to start using the loaded scripts.

    Also remember that you can setup scripts to load asynchronously using different techniques, "defer" is just one of them, but it is not evenly available on all browsers and where it is available there is nothing ensuring it will behave the same across different platform/browsers.

    The "doScroll()" fires before the onreadystatechange event is in the "complete" state and depending on the number of external binary files (stylesheets/scripts/images) it can fire several seconds before the "onreadystatechange" event. The complete "IEContentLoaded" solution is based on a combination of the doScroll() trick and the onreadystatechange event. It is not possible to solve all the cases with only the "doScroll()".

    Basically in IE the "doScroll()" is used when the page is not cached, while the "onreadystatechange" event is used when the page is already cached to ensure the notification always happens before the "onload" event. This is especially problematic when using the Back/Forward browser buttons to navigate through pages that are already cached.

    I fully understand the problem you are exposing, however that's not due to the "doScroll()" method, use asynchronous XHR requests to load your external scripts. You can do that as soon as javascript executes, no need to wait for ready/load notifications to do that.

    It is not the "doScroll()" blocking the load process, it is the exact contrary, the script execution is what is blocking/deferring the notifications. Both "dom:loaded" and "load".

  • Diego Perini

    Diego Perini June 21st, 2010 @ 05:14 PM

    @jdalton

    "The onreadystatechange will fire complete just before window.onload fires so that is no good."

    Correct ... it is not as good as the "doScroll()" for a primed cache hit.

    But as I explained above this does not cover when pages and external binaries are already cached, that's why I also use "onreadystatechange" as a fallback to catch those cases.

    In those cases, the risk of only using "doScroll()" is that it could fire after the "load" event (just few milliseconds) but being late in notifying that, can cause different unexpected behaviors in user scripts.

    The reasons "doScroll()" could fail in those cases is due to the fact the check is TIME driven by "setTimeout()" (25/50 msec) not EVENT driven by a system event.

  • Sergiu Dumitriu

    Sergiu Dumitriu June 21st, 2010 @ 06:57 PM

    The "DOMContentLoaded" event and its IE counterpart emulation ONLY depends on the DOM being complete as per the HTML5 specifications describing it. No dependencies exists about stylesheets being loaded/applied or scripts being loaded/executed or any other external binary requirement being available (img/object/applet etc.)

    I disagree here. The parser section of the HTML5 draft states, in section 8.2.6, The end ( http://dev.w3.org/html5/spec/Overview.html#the-end ) that:

    So, the HTML5 spec clearly states that all defered scripts must be executed before DOMContentLoaded is fired.

    The same algorithm also says that the document content readiness is set right before onload, (steps 7 and 8), so indeed the onreadystatechange event doesn't help speedup things that much in IE.

    What could be done to still respect defered scripts, is add a check for the readyState of all scripts. I already implemented something like this, I'll prepare a patch for review.

  • Sergiu Dumitriu

    Sergiu Dumitriu June 21st, 2010 @ 07:01 PM

    It is not the "doScroll()" blocking the load process, it is the exact contrary, the script execution is what is blocking/deferring the notifications. Both "dom:loaded" and "load".

    Yes, I know, I didn't mean it as "doScroll physically causes the browser to abort parallel download", but as "not being able to consistently receive the dom:loaded event causes developers not to use defer or other parallel download tricks".

  • John-David Dalton

    John-David Dalton June 21st, 2010 @ 07:32 PM

    The HTML5 draft spec also says it has to be flagged parser-inserted. I cannot determine if that means it has to be inserted via insertBefore()/appendChild() or some other mechanism. Also keep in mind this is draft spec and browsers may not follow it especially IE.

    Edited: Calling in @getify. loopsiloopsiloo!

  • Sergiu Dumitriu

    Sergiu Dumitriu June 21st, 2010 @ 07:42 PM

    parser inserted = seen in the HTML source

  • Kyle Simpson

    Kyle Simpson June 21st, 2010 @ 09:48 PM

    I would agree wtih Sergiu that "parser-inserted" seems to imply that the parser found the script-tag while parsing the source and inserted it into the DOM. I wonder if there's a complementary "script-inserted" flag that can/should exist to indicate scripts which are dynamically added to the DOM via script logic.

    In any case, I've long heard people assert that "defer=true" (and now async=true) are supposed to emulate the same behavior as using a script-loader to insert scripts dynamically. The behavior I'm referring to is that it unpins the loading/executing of the script from the rest of the page's behavior (namely, loading). I think in practice this is quite true, in that a script that is defer'd is not guaranteed to run before the dom-ready event has fired (though certainly before onload fires).

    Yes, the spec (for HTML5) indicates that defer=yes scripts should cause the delay of dom-ready until they finish. But that's a draft spec for an as-of-yet-incomplete HTML5 implementation. Far more practical is the pragmatic observation of how the browsers CURRENTLY do things.

    As for the doScroll() IE hack, according to IE internal documentation, it shouldn't make doScroll available until a very similar point in processing to what we'd call dom-ready. That's far from saying it's actually dom-ready. It's also far from saying that IE would have chosen to delay doScroll() intentionally to wait for defer scripts. In fact, again as practically observed, they do not.

    On the other hand, script loaders like my LABjs project have been able to reliably handle detecting when scripts finish, which is a far better "event" to wait on that some emulated/hacked dom-ready. If you're searching for better performance, which it sounds like you are, it would be much better to implement using a script loader like LABjs and execute initialization code when the scripts finish, regardless of what the rest of the page is doing.

    ONE NOTE: Prototype AFAIK is known to do what I call "unsafe" dom-ready detection in that, in FF, etc (not IE), it doesn't check for the presence of document.readyState already being set.

    To my knowledge, jQuery 1.4+ was the first (and only since) library that checked for document.readyState at run-time of the library, so that if the library was being loaded dynamically and the dom-ready even had happened to already be passed, then it would immediately set the internal "ready" flag to true and let any dom-ready waiting code fire immediately. For jQuery, this means that it can finally be loading dynamically (like by a script loader) without any fear of interferring with $(document).ready(...) blocks.

    However, as far as I know, Prototype cannot (yet) be reliably inserted dynamically into a document because it will not properly check for and recognize if dom-ready has already passed. So, my advice remains to be: 1) load prototype manually with a script tag (no defer!) 2) load everything else with LABjs.

  • Tobie Langel

    Tobie Langel June 21st, 2010 @ 11:48 PM

    • Tag changed from defer, dom:loaded, ie to section:dom

    Kyle, regarding your note regarding "unsafe" dom-ready, I find the behaviour you describe rather unexpected given how events work (past events aren't stored and re-dispatched to new subscribers).

    I think it's acceptable given jQuery's onReady API but would break POLS given Prototype's API.

    Maybe we should add something like this to Prototype (pseudo-code):

    Proto.onReady = function(callback) {
      if (document.loaded) {
        callback();
      } else {
        document.on("dom:loaded", callback);
      }
    };
    

    Thoughts?

  • Kyle Simpson

    Kyle Simpson June 22nd, 2010 @ 08:54 PM

    @Tobie- That's essentially how jQuery does it. The difference of course is the check on "document.loaded", which they check "document.readyState".

    I understand that it makes dom-ready a special type of event in that it immediately fires if already passed. But think about it... dom-ready is quite special... it only ever fires once. And there's lots of important code that waits on it.

    Moreover, we need a pattern like this (wait, or immediately execute if already fired) so that authors can write code that is robust enough to work no matter how their code is added to a page. Imagine this: I write a little plugin that pops up a cool little picture on a page when you hit the K key. Now, that plugin obviously needs to "wait" for dom-ready, as it's going to modify the DOM.

    But if you consider the plugin AND its library (prototype, jquery, etc) as a self-contained package (or an individual resource with dependencies, however you wanna look at it), that entire "package" needs to be present on the page.

    But, I as the plugin author have no idea how people will include it on pages. Someone may load it in via a blocking script tag. Someone else may dynamically load it with a script loader (like LABjs). And still someone else may dynamically add it to a page with a bookmarklet or other browser extension of some sort.

    In all cases, my code needs to say "hey, i just need to make sure dom-ready has passed before i do my thing. I don't care if it happened a long time ago, or hasn't happened yet. Just queue me up to execute for when it does, or now, whichever is later".

    The framework then needs to be able to detect if it itself was added to a page after the dom-ready has already passed. If so, it can safely signal to anyone later "yup, the dom is ready, go for it".

    And the only sensible way for me to hook my code into happening either now (dom-ready already passed) OR later (dom-ready happening in the future) is to attach a listener for the special "dom-ready" event, and assume the framework will do the execute-now-or-later magic for me.

    Otherwise, my plugin would have to be written custom for each usage pattern. This is not good.

  • Kyle Simpson

    Kyle Simpson June 22nd, 2010 @ 08:59 PM

    @Tobie- I should also say that I consider any dom-ready detection as "unsafe" if it uses any techniques which are not "safe" for the dynamic inclusion (script loader) use case. Not checking for document.readyState is one such problem many frameworks have. But another problem is use of things like document.write().

    It's fine if a framework wants to intentionally not support the idea of it being dynamically added to a page sometime well after page-load. But if it uses these techniques on purpose and ignores this increasingly popular/important use case of dynamic/on-demand loading, I call that "unsafe" dom-ready behavior.

    Hope that helps explain my position slightly better.

  • Tobie Langel

    Tobie Langel July 1st, 2010 @ 11:11 PM

    • State changed from “new” to “enhancement”
    • Milestone set to 2.0
    • Importance changed from “” to “Low”
  • sherymerykurian
  • ztwoem

Please Sign in or create a free account to add a new ticket.

With your very own profile, you can contribute to projects, track your activity, watch tickets, receive and update tickets through your email and much more.

New-ticket Create new ticket

Create your profile

Help contribute to this project by taking a few moments to create your personal profile. Create your profile ยป

The Prototype JavaScript library.

Shared Ticket Bins

Pages