A few days ago, Martín Borthiry contacted me with a question. He had been using the optimized asynchronous Google Analytics snippet for a while, and noticed an additional speed gain when wrapping it inside a setTimeout()
with a delay of 0 milliseconds. His tests made it pretty clear that this technique was indeed slightly faster, but Martín had no clue why.
The answer to his question is simple: JavaScript code inside setTimeout()
doesn’t necessarily delay the onload
event. Consider the following test case:
<!DOCTYPE html>
<meta charset="utf-8">
<title>setTimeout() and window.onload</title>
<script>
setTimeout(function() {
document.body.style.background = 'red';
}, 3000);
window.onload = function() {
document.body.style.background = 'green';
};
</script>
Here’s what happens when you open this document:
- The browser’s HTML parser does its thing, but halts as soon as it encounters the opening
<script>
tag. - The contents of the
<script>
element are executed. ThesetTimeout
will cause some other code to run in 3 seconds, and an event handler is bound towindow.onload
. - The HTML parser continues parsing the document until the end.
- Since there are no other resources on this page, the
onload
event fires as soon as the parser is finished. - The
window.onload
event handler is invoked. The document gets a green background. - About 3 seconds later, the
setTimeout
function kicks in. The document gets a red background.
In this example, using setTimeout
doesn’t delay the onload
event. Note that this is not necessarily the case for other scenarios! For example, consider the following document:
<!DOCTYPE html>
<meta charset="utf-8">
<title>setTimeout() and window.onload</title>
<img src="image.png">
<script>
setTimeout(function() {
document.body.style.background = 'red';
}, 3000);
window.onload = function() {
document.body.style.background = 'green';
};
</script>
As you can see, we’ve added an image to the document. (Of course, this could be any other resource that blocks onload
.) The onload
event won’t fire before the image is fully loaded.
This gives us a slightly different result:
- The browser’s HTML parser starts parsin’ away.
- The browser starts downloading the image as soon as the
<img>
element is parsed. - The parser halts as soon as it encounters the opening
<script>
tag. - The contents of the
<script>
element are executed. ThesetTimeout
will cause some other code to run in 3 seconds, and an event handler is bound towindow.onload
. - The HTML parser continues parsing the document until the end.
- As soon as the image is fully loaded, the
onload
event fires. - The
window.onload
event handler is invoked. The document gets a green background.
As you can see, one step is missing from this list, simply because there’s no way to accurately predict where it should go. The code within the setTimeout
will be executed 3 seconds after step 4, we know that much. But depending on how long it takes to download the image, this could be before or after onload
.
Let’s say the image takes 1 second to load. This means the onload
event will fire after about a second, too. Two seconds later, the code in the setTimeout
will finally get executed.
If the image takes 5 seconds to load, the onload
event will again be delayed during that time, so the code in the setTimeout
will be executed before onload
.
What would happen if the image takes about 3 seconds to load, i.e. the delay parameter for the setTimeout
? Will onload
fire before, during, or after the code in the setTimeout
?
To answer that question, you need to understand that JavaScript is single threaded. If there’s some code that is running (either from an inline script or from inside the setTimeout
) at the time that the browser is ready to fire onload
, the browser will have to ‘wait’ until the script is finished before onload
is processed. Similarly, if you use setTimeout
to download resources, and they enter the download queue before onload
fires, then loading these resources will (still) delay onload
. (Thanks to Kyle Simpson for explaining this in detail to me!)
In other words, using setTimeout
does not guarantee to speed up the onload
event in all cases. But most of the time, it will.
Useful?
This means we can use the setTimeout(fn, 0)
pattern to prevent delaying the onload
event, causing the perceived load/rendering time to decrease. Of course, this technique can only be used for scripts that aren’t dependencies.
I think tracking scripts make a good example. Most of those insert a new <script>
element into the DOM dynamically. As you know, every DOM operation comes with a certain performance penalty. The default Google Analytics code, for example, will modify the DOM as soon as the snippet is encountered, therefore delaying the onload
event.
Here’s a modified version of my asynchronous Google Analytics snippet, using setTimeout
to prevent delaying onload
:
<script>
var _gaq = [['_setAccount', 'UA-XXXXX-X'], ['_trackPageview']];
setTimeout(function() {
var g = document.createElement('script'),
s = document.scripts[0];
g.src = 'https://ssl.google-analytics.com/ga.js';
s.parentNode.insertBefore(g, s);
}, 0);
</script>
(Note that this is basically what Martín’s article is all about. Go check it out!)
Some caveats
As mentioned before, this technique cannot be used for scripts that are dependencies. It’s not very predictable when exactly the fn
inside setTimeout(fn, 0)
will be executed. If you had two or three of these constructions, there’d be no way to tell in which order they execute.
Also note that the this
binding of the function inside setTimeout
is automatically overridden to the global window
object. This may break scripts that rely on this
referring to something else at that point.
Note that the smallest setTimeout
timeout value allowed by the HTML5 specification is 4 ms. Smaller values (like 0
) should clamp to 4 ms. You can test which browsers follow the spec in this regard here.
Thoughts?
I really feel like this needs further investigation. I’d love to hear what you think in the comments!
Comments
fearphage wrote on :
FYI:
setTimeout(fn, 0)
is “slow”. Try to use a faster method (likepostMessage
).http://dbaron.org/log/20100309-faster-timeouts
http://dbaron.org/mozilla/zero-timeout
Mathias wrote on :
fearphage: Thanks for those links!
It sounded like a good idea to take that snippet and make it backwards-compatible, using
setTimeout(fn, 0)
as a fallback in casepostMessage
is not supported. So I did: http://gist.github.com/579895 Here’s a demo: https://mathiasbynens.be/demo/setzerotimeoutI hardly tested this, so consider it a work in progress.
Mathias wrote on :
fearphage: Based on my demo, it looks like
setZeroTimeout
delaysonload
in Firefox 3.6.9, Opera 10.62, IE8, and IE9pre4 (but oddly not in IE7 or IE6!). Can you confirm this? Is there a flaw in my test case?setTimeout(fn, 0)
may be slow, but at least it prevents delayingonload
.Aaron wrote on :
You wrote “using
setTimeout
does not guarantee to speed up theonload
event in all cases. But it might just do so”.Do you agree that it’s actually better to say “…but it probably will do so”?
Mathias wrote on :
Aaron: Yeah, it probably will in most cases. I’ll change that in the article, thanks!
philip wrote on :
If you use
setTimeout
to download resources, and they enter the download queue beforeonload
fires, then it will (still) delayonload
.Mathias wrote on :
philip: Good point. I guess that wasn’t really clear from the article. I’ve now updated the text to explicitly mention this. Thanks!
Dan Beam wrote on :
— Steve Souders’ comment on this article
Mathias wrote on :
Dan: Yeah, that’s what I was saying here:
ionut popa wrote on :
Hi Mathias, you mentioned:
Why is this true? Because of the loading indicator disappearing or for some other reason?
Here it’s mentioned that making the
load
event fire faster won't improve the user experience much: http://queue.acm.org/detail.cfm?id=2446236Mathias wrote on :
ionut popa: That is explained in the article. When running
setTimeout(fn, 0)
, the code infn
won’t be executed “immediately” and thus it won’t necessarily delay theload
event. On the other hand, if the code infn
starts executing before theload
event (e.g. if you hadn’t usedsetTimeout
), theload
event will fire after the code infn
— all of it — has finished.zcorpan wrote on :
setTimeout
is only clamped if it’s invoked within asetTimeout
.Sanjay Kumar Moraniya wrote on :
Thank you very much. Very good notes.