Out of Hanwell

June 8, 2006

The window.onload Problem Revisited

Filed under: JavaScript — Matthias Miller @ 6:01 pm

Today I was researching ways to provide Mozilla’s DOMContentLoaded functionality in Internet Explorer. Dean Edwards has already demonstrated two ways that this can be done, using the script “defer” trick and behaviors. Both methods require that an external file be included–either “ie_onload.js” or “loaded.htc”.

I need this feature for a JavaScript library, so I specifically want it to be available without forcing the user to include these external files. I also want it to be self-contained so that a developer can drop a single script into a page or site to get the DOMContentedLoaded-like functionality.

Approach A: JavaScript URL

After a lot of hunting, it finally occurred to me that the script’s “src” attribute can reference JavaScript code. The simplest approach is to replace ie_onload.js with a JavaScript URL. Instead of this…

<!--[if IE]><script defer src=”ie_onload.js“></script><![endif]–>

…you use this:

<!--[if IE]><script defer src=”javascript:’init()’“></script><![endif]–>

Dean Edwards commented on his blog that the “defer” technique only works with external scripts. At first glance, this seems to contradict my findings. But I think he only meant that the “defer” technique requires that the script use the “src” attribute (vs. an inline script) and not that the code must be contained in an external file.

It is important to note that the init function is included in quotes. Internet Explorer will evaluate the JavaScript expression in the src attribute and use the result as the contents of the <script> tag. Because Internet Explorer evaluates the src attribute immediately, the init function must be called from the script itself.

The biggest problem I discovered is that this approach is broken in Internet Explorer 7 Beta 2. However, there is a bug filed for it, so if you’d like to use this, go vote for a fix!

Approach B: Using the script’s onreadystatechange

I also pursued an alternative approach. While I was perusing MSDN’s documentation on the SCRIPT element, I found that it also supports the onreadystatechange event. Because Internet Explorer waits to load these scripts until the DOM has loaded, the page can watch for changes to the script’s readyState. When the script starts loading, the page can trigger DOMContentLoaded initialization.

Even though IE7b2 does not execute the JavaScript in the src attribute, it does trigger onreadystatechange correctly (fortunately!). Unlike the previous example, this code does not need any conditional comments since onreadystatechange will only be called in IE.

Scripts created with createElement do not respect the defer attribute, apparently because they are loaded (although not executed) before the script is attached to the document. Scripts created with innerHTML must be preceded by a node of a certain type, and they appear to suffer the same problems as createElement.

Given these findings, the init.js script would look something like this:

document.write('<script id="__init_script" defer="true" src="//[]"></script>');

function registerInit(callback) {
/* for Mozilla */

if (document.addEventListener) {
document.addEventListener("DOMContentLoaded", callback, false);
}

/* for Internet Explorer */

if (document.getElementById) {
var deferScript = document.getElementById('__init_script');
if (deferScript) {
deferScript.onreadystatechange = function() {
if (this.readyState == 'complete') {
callback();
}
};

/* check whether script has already completed */
deferScript.onreadystatechange();

/* clear reference to prevent leaks in IE */
deferScript = null;
}
}

/* for other browsers */
window.onload = callback;
}

The code in the HTML file would look like this:

function init() {
// quit if this function has already been called
if (arguments.callee.done) return;

// flag this function so we don't do the same thing twice
arguments.callee.done = true;

// do initialization here
};

registerInit(init);

Disclaimer

Although this script seems to work for me, I place no guarantee on it. I know that Dean Edwards has tried more things than I can imagine, so I’ll be slow to call this the final solution. I’m very interested in criticism.

I should also note that this is not the actual code that I will be using in the library. It needs to support multiple event handlers, and it should call the event handlers only once. I have not included these details for simplicity.

Update: I fixed both approaches, which were broken. Although the onreadystatechange approach is not as robust as it could be (it would be nice not to depend on document.write), it does seem to be a step in the right direction.

Update: Applying fixes for HTTPS.

36 Comments »

  1. Fab! I came across this the other day too:

    http://linguiste.org/projects/behaviour-DOMContentLoaded/example.html

    Like you, I need to include something like this in a library but don’t want to be dependent on external files. This is perfect.

    Comment by Dean Edwards [Visitor] — June 9, 2006 @ 2:00 am

  2. deferScript.src = 'javascript:void 0';

    void 0?

    How does that translate into void(init())

    Comment by Jon B [Visitor] — June 10, 2006 @ 3:13 am

  3. The magic in the second example is not in the script’s src. As you noticed, it does not call the init function. In fact, it does nothing at all. Instead, the registerInit function uses onreadystatechange to determine when Internet Explorer starts loading the script, even though the script will do nothing. At that point, the onreadystatechange handler calls init, which is passed as a references via the callback parameter.

    Comment by Matthias Miller [Member] — June 10, 2006 @ 8:18 am

  4. I’m not sure that this technique works. It seems to fire too early for me and I’m getting an incomplete DOM. :-(

    Comment by Dean Edwards [Visitor] — June 12, 2006 @ 4:09 am

  5. The revised solution works perfectly. Kudos to you sir! :-)

    Comment by Dean Edwards [Visitor] — June 15, 2006 @ 3:43 pm

  6. Hey, thanks…glad to hear that!

    Comment by Matthias Miller [Member] — June 15, 2006 @ 3:55 pm

  7. I turned yours and Dean’s script into an Object Literal you can drop into any file and pass an object of choice to it.

    Needs testing but I like that flexibility.

    Comment by Rob Cherny [Visitor] — June 17, 2006 @ 7:10 am

  8. This may work well as a replacement in your test cases. Would you try it ?

    var onDOMLoaded=function(f,t){
    if(typeof document.getElementsByTagName!='undefined' && (document.getElementsByTagName('body')[0]!=null || document.body!=null)){
    var h=document.getElementsByTagName(’HTML’)[0];
    var s=document.createElement(’script’);
    s.type=’text/javascript’;
    h.appendChild(s);
    s.text=f+’()’;
    }else if(100>t){
    setTimeout(function(){onDOMLoaded(f,t)},t);
    if(10>t){t++;}else{t+=10;}
    }
    };

    Call it passing the function name (String) and a 0 (Int), like this:
    onDOMLoaded(’init’, 0);
    I also posted this on Dean site.
    Your comments appreciated.

    Comment by Diego Perini [Visitor] — June 18, 2006 @ 2:03 am

  9. Diego, the BODY element will exist before the complete DOM has been loaded, so this approach doesn’t work. To test, just set up a server-side script that sends the first half of the page immediately and waits to send the second half. That will make the problem abundantly clear.

    Comment by Matthias Miller [Member] — June 21, 2006 @ 5:57 am

  10. Thank you Matthias, you are correct up to this point:

    >> Diego, the BODY element will exist before the complete DOM has been loaded,

    BUT if you look closely at the code the events are not fired at that moment.

    At that moment I only insert a new SCRIPT element at the end of the page, and that SCRIPT will be parsed/executed only after all the elements preceding it have been setup in the DOM. At that time the events are fired.

    If you are willing to try, substitute two lines in the above example:

    h.appendChild(s);
    s.text=’alert((document.all||document.getElementsByTagName(”*”)).length);’

    instead of calling the init() function we alert() the number of elements found in the documents.

    This is for me the only reliable way to test if all (i will prefer to still say many) elements are already in the DOM…

    I will publish and send links to the test pages I have, there are Dean testcase and a little of my own.

    Matthias, I really need and appreciate your testing. I will not make you loose time…promise.

    If it doesn’t work for you with Safari, use “s.innerHTML” instead of “s.text” in the above and move the line before the appendChild.

    I am using Safari on PearPC emulator (really slow…but help debugging it better) so I tested.

    I am also testing at the same time 4 Browsers native Linux (FF/Moz/Opera/Konq) the same (no Konq) on Windows and on Mac under PearPC.

    My latest tests on Safari shows that it is really hard to be called before the normal “onload” event but that may depend on my slow emulation.-

    Please keep commenting…I need it, good or bad.

    Diego

    Comment by Diego Perini [Visitor] — June 21, 2006 @ 10:32 am

  11. I thought I will better post this one with the debug line commented.
    Remove the comments on “var l=” and put them on the next “var l=” line to see what I was talking in the previous post. It will print the number of elements loaded so far. It seems all of them.

    I had to resource to Safari sniffing (sigh…) I could not find a better solution at the moment.
    I also corrected the testing, it seems more appropriate, there should be more than 2 elements.

    You now have to pass the window, I need that for several frames inside the same window, but you can adjust it to your taste with the name of a function as it was before if you prefer.

    var onDOMLoaded = function(w, n) {
    var d=w.document
    if (typeof n == ‘undefined’) { n = 0; }
    if (d.body && (d.all||d.getElementsByTagName(’*')).length > 2) {
    // var l=’var cc=document.all||document.getElementsByTagName(”*”);alert(cc.length);’;
    var l=’init();’;
    var h = d.getElementsByTagName(’html’)[0];
    var s = d.createElement(’script’);
    s.type=’text/javascript’;
    if (/WebKit/i.test(navigator.userAgent)) {
    s.innerHTML=l;h.appendChild(s);
    }else{
    h.appendChild(s);s.text=l;
    }
    } else if(1000 > n) {
    setTimeout(function() { onDOMLoaded(w, n); }, n);
    if (10 > n) { n++; } else { n += 10; }
    }
    };
    onDOMLoaded(this);

    You have to call onDOMLoaded(this) in each document or iframe document where you want to use this event capability.

    “DOMContentLoaded” should be preferred to this where applicable (FF/Moz/Opera9 at the moment).

    Diego

    Comment by Diego Perini [Visitor] — June 21, 2006 @ 11:42 am

  12. Diego,

    At that moment I only insert a new SCRIPT element at the end of the page, and that SCRIPT will be parsed/executed only after all the elements preceding it have been setup in the DOM. At that time the events are fired.

    If the script loads only after preceding elements have been set up in the DOM, it means that you should only add your script to the page after the DOM has fully loaded. This means that you still need a way of finding out when the DOM has fully loaded so that you can add this script to the document. This is the fundamental problem that keeps your code from working.

    Do you not have access to a copy of Internet Explorer? When I try running your script, I get an “Operation Aborted” error because it appends a script to the HTML element before all of its children have been loaded. This is another significant problem in this approach.

    Do yourself a favor by setting up a page in PHP, Ruby, Perl, Python, or the language of your choice to send the page in two halves, separated by a one- or two-second delay. As I said, this will make it abundantly clear whether the approach really works.

    Comment by Matthias Miller [Member] — June 21, 2006 @ 12:01 pm

  13. You have been correct in everything you said Matthias…

    I have setup a PHP with some output then a 1/2 sec delay then a second output as you instructed.
    I had to disable “output_buffering” in PHP to see the problem you are talking about.

    The SCRIPT does not fire when all the page loaded…you are completely right. It fires in the middle. With PHP “output_buffering=4096″ (the default) the script worked I mean it displayed the exact number of elements, also this is due to the size of the page and everyone’s “output_buffer” setting so not reliable.

    At least we can say this solution allows user handlers to fire before the standrad “onload” in all browsers except Safari (up to now). Can you confirm this ?

    Here are the test links to not working test:

    http://javascript.nwbox.com/onDOMLoaded/matthias.php

    the working (expected behavior) using ‘DOMContentLoaded’ (only FF 1.5.x and Opera 9):

    http://javascript.nwbox.com/onDOMLoaded/matthias2.php

    Thank you for making this clearer to me. With small files the “output_buffering” thing obfusacated some of my tests. I am going to fix this in my application if at all possible.

    Do you have a script that does this on some browser platform without using ‘DOMCOntentLoaded’ ?

    The only resort will be a “HEAD” to the server with XMLHttpRequest to know the lenght of the page then check with the length of “document.documentElement.innerHTML”. I just hope there is enough time for the “HEAD” response to come back…that probably depend on how big a page is.

    I now realize it was not necessary to react so early in some situations. In these situations however I prefer to have a PHP assemble all my scripts in a big one gzip it and send compressed, this solves many other situations but not this exact one.

    But really, if you do not have some known variable like byte size of the file, or number of elements to wait for, or last element to wait for, then there is definitely no solution to this exact problem (tell me I am wrong and I will be very happy).

    The easiest I have seen for this is to insert your call to the init() function in a script at the end of the page, and no you do not have to do it in each page, you can have PHP auto_append the script with the call to your init().

    Thank you again for your time, at that address I’am setting up things to test my addEvent() which has now become rock solid. It would be great is with time permitting you could also look at that.

    Diego Perini

    Comment by Diego Perini [Visitor] — June 21, 2006 @ 2:40 pm

  14. I’m glad you were able to duplicate the problem, Diego. You might want to take a look at jquery’s implementation, which uses document.readyState for Safari. I believe jquery supports all major browsers.

    Appending the init() function to the end of every page works fine, except for scripts that depend on the value of form controls when the back button is pressed in Internet Explorer. (The form values are restored after the DOM is loaded, so they will not contain the expected values when the init function is called with this approach.) This may not be a problem, but you should be aware of this weakness.

    Comment by Matthias Miller [Member] — June 21, 2006 @ 6:02 pm

  15. I just finished working on the method I was talking about reading the file size Matthias….

    I will surely look in the jQuery code even if if you did not tell anything about how they did.
    This means that a method already existed…

    However the code I just assembled works now, tested with the PHP code that did not work in the last sample I submitted (the one with the delay in between outputs). Also Safari/PearPC works.

    In a couple of minutes you will have the links. I am swapping out the old examples.
    I am sorry to be so invadent with your time, but you words where gold for me.

    And I think the code is worth a look. I will also put a “busted3.html” copy of Dean Edwards modified with this method and see if the results are the same.

    I send a ‘HEAD’ to discover the size, if ‘Content-length’ is not present in the response headers then it is a dynamic page in PHP, Python whatever then I do a GET to discover the size.

    The rest is easy, when the size of “innerHTML” is bigger then what we expect the events get fired.

    OK, you can test it at this address:

    http://javascript.nwbox.com/onDOMLoaded/

    I am setting it up now, sorry for any inconvenience.

    Comment by Diego Perini [Visitor] — June 21, 2006 @ 7:03 pm

  16. I took this solution from Dean’s site, mixed in a little of Dan Webb’s Prototype extension, sprinkled with a dash of Simon Willison’s addLoadEvent and baked for 5 minutes. The result: addDOMLoadEvent, a function which lets you register multiple onLoad functions…

    http://www.thefutureoftheweb.com/blog/2006/6/adddomloadevent

    Comment by Jesse Skinner [Visitor] — June 21, 2006 @ 11:56 pm

  17. Well you will have realized english is not my virtue…so many errors, so the meat:

    http://javascript.nwbox.com/onDOMLoaded (updated)

    I am getting very good results with this FILE SIZE method. I believe to have solved a nasty effect of Opera not showing scripts and stylesheets in the ‘innerHTML’, so let’s try it with various browsers I have tested 7 of them (4 on Linux, 2 on Windows and Safari/PearPC) all are working with the tests cases, even with the PHP delay in between outputs as Matthias suggested as ‘good proof’.

    But it’s still aproximate about 200/300 bytes. The most of it is the DOCTYPE, the HTML tag and it’s attributes, some of them may be rebuilt for better accuracy by using properties like:

    document.doctype.publicID
    document.doctype.systemID

    still I don’t know how many browsers have those properties.

    The previous onDOMLoaded was also integrated in the Event system so you could add more than one event, and especially in different IFRAMES. I am working on it. Comments please.

    Comment by Diego Perini [Visitor] — June 22, 2006 @ 5:12 pm

  18. Matthias, this is a first try in to getTail(), something that will retrieve just few bytes at the end of the URL Resource, so we finally will have a way of seeing when the stream is finished.


    function getTail() {
    var r = null, s = null;
    try { r = new XMLHttpRequest(); }
    catch(e) { r = new ActiveXObject((navigator.userAgent.toLowerCase().indexOf('msie 5') != -1) ? 'Microsoft.XMLHTTP' : 'Msxml2.XMLHTTP'); }
    r.open('GET', d.location.href, false);
    r.setRequestHeader('Range', 'bytes=-512');
    r.send(null);
    return r.responseText;
    }

    What do you think about this ?

    I made some tests online on my web space at http://javascript.nwbox.com, have a look. The test case you suggested have showed it’s importance during my coding. Thank you again.

    Diego

    Comment by Diego Perini [Visitor] — June 22, 2006 @ 7:08 pm

  19. On Dean Edwards’ blog, Mario said:

    For https use what devs use for the iframe issue (instread of src=javascript:void(0); use src=javascript:false;

    At: http://dean.edwards.name/weblog/2006/06/again/#comment5788
    Does anyone know if this works?

    Comment by Tanny O'Haley [Visitor] — June 27, 2006 @ 10:14 pm

  20. I just tried “javascript:false”, and I get the secure/nonsecure warning.

    Comment by Matthias Miller [Member] — June 28, 2006 @ 6:56 am

  21. Matthias, I just sent you e-mail with a tentative solution for the “onload”
    problem related to form values and the back-button in IE.

    It may probably be used as a replacement for the DOMLoaded, since when
    FORMS are ready the BODY will surely also be there…

    This type of event (not a real event) will be fired when all the filled-in
    form values have been restored back in their respective fields.

    I have setup test files at these URLs.

    http://javascript.nwbox.com/formsLoaded/ie-formvalues.htm
    http://javascript.nwbox.com/formsLoaded/ie-formvalues.php

    The example in PHP has several delays in between outputs to test it works correctly.

    Please comment.

    Comment by Diego Perini [Visitor] — July 1, 2006 @ 6:00 am

  22. This solution for https works, tested on IE7, IE6 and IE5.5 over 80 and 443, firing correctly onreadystatechange:

    src=https:javascript:false

    https:/// was a suggested solution, but caused errors on https pages (although OK on http).

    Comment by Alistair Potts [Visitor] — July 6, 2006 @ 5:15 am

  23. @Alistair: Specifically what error did you receive when testing with https:/// ? In which browsers did you receive the error? Do you have a test case demonstrating the problem?

    I tested https:/// in the three browsers that you mentioned, and it worked as expected.

    Comment by Matthias Miller [Member] — July 6, 2006 @ 5:57 am

  24. I should have said: https:/// only FAILS in IE7 (which is in late beta but it’s probably not worth assuming it will be fixed). You have to make sure you have “Display a notification of every script error” on. I tried both escaping and not escaping the forward slashes in the write.

    The unhelpful error is:

    A runtime error has occurred. Do you wish to debug?
    Line: 1
    Error: Syntax Error

    I believe that ‘Line 1′ is Line 1 of the non-existant ‘file’ “https:///”. In other words, IE7 is interpreting “https:///” as a URL or file it should be looking for, or maybe a line of javascript, whereas previous IEs didn’t.

    The solution I posted works in IE7, 6 and 5.5. In fact the non-sensical “src=https:j” works just as well; “src=https:” will work in IE7 but not 6. But I’m also testing succesfully no src at all, which is surely the best solution:

    document.write(”");

    test case: https://www.partyark.co.uk/html/ieready.html

    Alistair

    Comment by Alistair Potts [Visitor] — July 6, 2006 @ 7:14 am

  25. Oops, don’t know where the code went - anyway, it was just showing the document.write without the src bit.

    A

    Comment by Alistair Potts [Visitor] — July 6, 2006 @ 7:18 am

  26. Your test case has no src attribute, fires too soon, and generates no runtime error. Please upload a test case that demonstrates the runtime error in IE7b3.

    Also, I suggest you read my advice about testing.

    Comment by Matthias Miller [Member] — July 6, 2006 @ 8:01 am

  27. Sure, you’re quite right, without src it doesn’t work. Mea culpa.

    Here are the test cases. The most byte-friendly solution I can find is src=0. Works over 443 and 80.

    https://www.partyark.co.uk/html/ie7b3success.asp
    https://www.partyark.co.uk/html/ie7b3fail.asp

    Alistair

    Comment by Alistair Potts [Visitor] — July 6, 2006 @ 9:12 am

  28. Your proposed solution works only because https://www.partyark.co.uk/html/0 does not exist. I will update my post with a working solution soon.

    Comment by Matthias Miller [Member] — July 6, 2006 @ 1:25 pm

  29. hmmm…
    great code examples!

    … just wondering as I walk through the original example, isn’t there a logic error
    by not having ELSE clauses… since IE (and Mozilla) will ALSO unconditionally execute
    the following, prematurely:

    /* for other browsers ((and with “fall-through logic bug”, including IE and Mozilla)) */
    window.onload=callback;

    that might explain some of the latter threads, too.

    DaveZ

    Comment by DaveZ [Visitor] — November 11, 2006 @ 5:07 pm

  30. [...] Filed under: Internet Explorer — outofhanwell @ 2:36 pm Rob mentioned that the window.onload solution generates a secure/nonsecure warning when used over a secure connection. This is because the [...]

    Pingback by Using window.onload over HTTPS « Out of Hanwell — September 20, 2007 @ 10:03 pm

  31. [...] Development — outofhanwell @ 6:10 am Have you ever felt like this? It reminds me of the window.onload problem. Big problems are terrifying. There’s an almost physical pain in facing them. It’s like [...]

    Pingback by Big Problems « Out of Hanwell — September 20, 2007 @ 10:04 pm

  32. IE/DOMLoad : behavior expression

    IEの独自実装による document.write を使わない DOMロード の検知方法を考えました。
    CSSのbehaviorとexpressionを使います。
    … document.documentElement.style.setExpression(’behavior’, ‘document.documentElement.polli…

    Trackback by juce6ox — December 13, 2007 @ 3:15 am

  33. [...] total solution has been crafted by the venerable Dean Edwards who pulled concepts from the solution engineered by Matthias Miller. Another possibility is this one, which is Diego Perini’s that is used by [...]

    Pingback by Update: Fixing Sys.Application.initialize « See Joel Program — June 11, 2008 @ 1:45 am

  34. [...] This is much neater and uses a nice regular expression to remove the class name. It also uses Microsoft’s attachEvent, which is much easier to make non-distructive. There is still a problem with this: the code only executes when the page is fully loaded. What we really need is to wait until the DOM is ready to be manipulated, then run the code. Unfortunately, as we’re aiming this code at IE6, we can’t just do a document.addEventListener(”DOMContentLoaded”, sfHover). The only way we can get a DOM ready event is by creating an empty script with defer=”defer” and listen for it to load, then run our code. This was thought up by Matthias Miller. [...]

    Pingback by Son of Suckerfish dropdowns in jQuery :: CreateOpen — September 22, 2008 @ 5:58 pm

  35. I’am getting this error “operation aborded”, then i have using this code

    window.onload = function(){
    }

    all is ok now, but i must wait that all the webpage is loaded for javascript execute my script…

    Is there any other way ?

    Comment by coupon — September 30, 2008 @ 6:18 pm

  36. Hi, Matthias and all!

    This is a fully validated and verified version, it does not need
    conditional hacks, iframe hacks, https hacks!
    It has NO memory leaks and allows multiple event functions
    to be executed BEFORE the document.body or document.images are fully loaded,
    as well there is no need
    to write those functions in a separate script and/or to put inside the
    ending BODY tag! -
    instead, all should be in ONE script before ending HEAD tag:

    see the working test page * http://arieslink.sitebar.org/onload-1.html
    (the script there is internal, look at the source code, minimal changes
    were done to Jesse Skinner’s work, based in its turn on yours,
    but all errors and warnings were eliminated)

    Best regards,
    Vlad

    Comment by Vlad Kout — October 11, 2008 @ 6:33 pm

RSS feed for comments on this post. TrackBack URI

Leave a comment

Blog at WordPress.com.