Onload in Onload
or “Why you should use document.readyState”
I asked several web devs what happens if an onload handler adds another onload handler. Does the second onload handler execute?
The onload event has already fired, so it might be too late for the second onload to get triggered. On the other hand, the onload phase isn’t over (we’re between loadEventStart and loadEventEnd in Navigation Timing terms), so there might be a chance the second onload handler could be added to a queue and executed at the end.
None of the people I asked knew the answer, but we all had a guess. I’ll explain in a minute why this is important, but until then settle on your answer – do you think the second onload executes?
To answer this question I created the Onload in Onload test page. It sets an initial onload handler. In that first onload handler a second onload handler is added. Here’s the code:
function addOnload(callback) { if ( "undefined" != typeof(window.attachEvent) ) { return window.attachEvent("onload", callback); } else if ( window.addEventListener ){ return window.addEventListener("load", callback, false); } } function onload1() { document.getElementById('results').innerHTML += "First onload executed."; addOnload(onload2); } function onload2() { document.getElementById('results').innerHTML += "Second onload executed."; } addOnload(onload1);
I created a Browserscope user test to record the results and tweeted asking people to run the test. Thanks to crowdsourcing we have results from dozens of browsers. So far no browser executes the second onload handler.
Why is this important?
There’s increasing awareness of the negative impact scripts have on page load times. Many websites are following the performance best practice of loading scripts asynchronously. While this is a fantastic change that makes pages render more quickly, it’s still possible for an asynchronous script to make pages slower because onload doesn’t fire until all asynchronous scripts are done downloading and executing.
To further mitigate the negative performance impact of scripts, some websites have moved to loading scripts in an onload handler. The problem is that often the scripts being moved to the onload handler are third party scripts. Combine this with the fact that many third party scripts, especially metrics, kickoff their execution via an onload handler. The end result is we’re loading scripts that include an onload handler in an onload handler. We know from the test results above that this results in the second onload handler not being executed, which means the third party script won’t complete all of its functionality.
Scripts (especially third party scripts) that use onload handlers should therefore check if the onload event has already fired. If it has, then rather than using an onload handler, the script execution should start immediately. A good example of this is my Episodes RUM library. Previously I initiated gathering of the RUM metrics via an onload handler, but now episodes.js also checks document.readyState
to ensure the metrics are gathered even if onload has already fired. Here’s the code:
if ( "complete" == document.readyState ) { // The page is ALREADY loaded - start EPISODES right now. if ( EPISODES.autorun ) { EPISODES.done(); } } else { // Start EPISODES on onload. EPISODES.addEventListener("load", EPISODES.onload, false); }
Summing up:
- If you own a website and want to make absolutely certain a script doesn’t impact page load times, consider loading the script in an onload handler. If you do this, make sure to test that the delayed script doesn’t rely on an onload handler to complete its functionality. (Another option is to load the script in an iframe, but third party scripts may not perform correctly from within an iframe.)
- If you own a third party script that adds an onload handler, you might want to augment that by checkingÂ
document.readyState
 to make sure onload hasn’t already fired.
Philip Tellis (@bluesmoon) | 17-Sep-14 at 6:48 am | Permalink |
Hi Steve,
The addEventListener spec states that if an event handler is added via code that runs as part of that event, then the new handler will only be called on subsequent invocations of the event. Since onload can only fire once, this means that onload handlers added in onload will never be called (assuming browsers follow the spec for this).
I noticed this when we started loading boomerang using the iframe loader technique, since there was now a possibility that it could load after onload (and well, some of our customers load boomerang in the onload event). As a result, boomerang has been using document.readyState for a while now to check if onload has already fired when it loads up.
Steve Souders | 17-Sep-14 at 8:45 am | Permalink |
Philip: I tested Boomerang while writing this post and was pleased to see it used readyState to avoid this problem. You’re always one step ahead.
Allen Lee | 26-Sep-14 at 12:01 pm | Permalink |
Great post.
So why not use document.readyState directly and skip onload and addEventListener entirely? IE
var f = function() { …. };
function fAfterLoad() {
if (document.readyState == “complete”) f();
else window.setTimeout(fAfterLoad, 250)
};
fAfterLoad()
The use case is a third party snippet for analytics that should be transparent to the user, i.e. google analytics. This has the advantage of being compatible with all browsers without special hacks for IE8 and not over writing window.onload, which could be used by the page.
Steve Souders | 26-Sep-14 at 12:34 pm | Permalink |
It’s more overhead to have a timer looping like that.
Laurens van Hees | 12-Oct-14 at 11:34 pm | Permalink |
Another tip, when the Navigation Timing API is available you can do something like this as well:
var isLoadFired = window.performance.timing.loadEventStart > 0;
Saikat | 21-Oct-14 at 4:41 am | Permalink |
I don’t know how good it is but can’t we fire another onload event i.e so that the second window onload function gets fired by the fake load event. like this
window.onload=function()
{
alert(“onload1”);
window.onload=function(){
alert(“onload2”);
}
var evt = document.createEvent(‘Event’);
evt.initEvent(‘load’, false, false);
window.dispatchEvent(evt);
}
//so both alerts are shown