About document.ready
There is some effort going on, documented the WHATWG standard repository, in order to bring a document.ready
Promise like behavior to any page.
Today, a simple tweet of mine simplified the fuss:
— Andrea Giammarchi (@WebReflection) May 9, 2016
document.ready = new Promise(r => /^loaded|complete$/.test(document.readyState) ? r() : document.addEventListener('DOMContentLoaded', r));
Regardless it was a proof-of-concept wrote to fit in a tweet, somebody started wondering about document.readyState
and the check I was doing: is interactive
missing? Should interactive be there? I usually target many more browsers than the average developer does, including IE9 or 10, but sometimes even IE8, plus many WebKit based die-hard mobile browsers.
Instead of explaining again how many inconsistencies there are around readyState
, I’ve simplified the tweet:
— Andrea Giammarchi (@WebReflection) May 10, 2016
document.ready=new Promise(r=>document.addEventListener('DOMContentLoaded',r))
on top of your page and no need to debate about readyState.
This is still a proof of concept wrote to fit on a tweet, and this post is about to provide a full, self-polyfilled version, of the very same concept, that would work today on your production site.
Using one script on top
As simple as it sounds, and since most modern sites are template or component based, this is the easiest solution to use: a script on top of the page.
The following is a “magnified“ version of how an HTML page would look like:
<!DOCTYPE html>
<html>
<head>
<script>
// on top of each page
(function (document, Promise) {
// if the native implementaion is available
// do nothing. Otherwise ...
'ready' in document || (
// create a new Promise
document.ready = new Promise(
function (resolve) {
// that will resolve
document.addEventListener(
// once DOMContenLoaded happens
'DOMContentLoaded',
resolve,
// and only once (where supported)
{once: true}
);
}
)
)
}(
document,
this.Promise ||
// ad-hoc Promise fallback
// this is not supposed to be a Promise
// polyfill or replacement
// it just covers, with a minimal amount of code,
// the common document.ready.then(...) case
function (callback) {
var
// stores all callbakcs registered
// while the DOMContentLoaded hasn't happened yet
queue = [],
// will store the DOMContentLoaded event once it happens
result
;
// once the DOMContentLoaded is triggered
// store the event like native Promise would do
callback(function (e) {
result = e;
while (queue.length) {
queue.shift()(result);
}
});
// the only method available through this instance
this.then = function (callback) {
// if result already set as event
// trigger the callback ASAP (but not synchronously)
if (result) setTimeout(callback, 0, result);
// otherwise add to the queue of callbacks
else queue.push(callback);
// for "thenability" sake return same instance.
// Bear in mind this is not a new Promise,
// as it would be if it was a native one.
return this;
};
}
));
</script>
</head>
<body>
<script>
// this is just for testing purpose
document.ready
.then(function () {
document.body.textContent = 'document';
})
.then(function () {
document.body.textContent += '.ready';
});
// fake a lazy loaded script
setTimeout(function () {
document.ready
.then(function () {
document.body.textContent += ' \\o/';
});
}, 1000);
</script>
</body>
</html>
A minified version of the same code
Dropping comments and long names, here is how the script would actually really look like:
!function(d,P){'ready'in d||(d.ready=new P(function(r){d.addEventListener('DOMContentLoaded',r,{once:!0})}))}
(document,this.Promise||function(c,q,r){q=[];c(function(e){for(r=e;q.length;)q.shift()(r)});this.then=function(c){r?setTimeout(c,0,r):q.push(c);return this}});
You could save the script a part and still be sure it’s the only one on top that’s blocking so that every other script can simply do the following:
document.ready.then(function () {
alert('Yeah!');
});
If you care about removing the DOMContentLoaded listener
In case you are wondering what is that {once: true}
object used as third argument of addEventListener
, you might like to discover there are changes in that API.
However, to bring these changes in and normalize much more, the dom4 polyfill would be my best suggestion. This, at least until all your target browsers support this new feature.
Put the following script even before the inline one as only mandatory 4.5KB dependency for your pages and you’re good to go.
<script src="//cdnjs.cloudflare.com/ajax/libs/dom4/1.8.3/dom4.js"></script>
If you care about IE9
Legacy IE < 10 have a non standard setTimeout
and setInterval
that won’t pass extra arguments when used. If you care about fixing them, you can use legacy IE conditional comments that are completely ignored and transparent for the entirety of the browsers out there.
Add the following comment before the script and you’ll be good to go.
<!--[if lte IE 9]><script>(function(w,f){w.setTimeout=f(setTimeout);w.setInterval=f(setInterval)})(window,function(f){return function(c,t){var a=[].slice.call(arguments,2);return f(function(){c.apply(this,a)},t)}});</script><![endif]-->
Bear in mind that conditional comment is also the best place to eventually put es5-shim and es5-sham so that the rest of the code will most likely work once transpiled.
If you care about IE8
The addEventListener
and DOMContentLoaded
event are not present in IE8. Be sure you add the following conditional comment so that only IE8 will be affected, downloading its own polyfill from a CDN.
<!--[if lt IE 9]><script src="//cdnjs.cloudflare.com/ajax/libs/ie8/0.4.1/ie8.js"></script><![endif]-->
At this point every single browser on this planet should be compatible with this document.ready
, and you can test the demo page to confirm it.
What’s cool about DOMContentLoaded
There are many reasons to use DOMContentLoaded as entry point for anything JS, or progressive enhancement, related:
- it’s one of the most battle-tested events of all times
- it’s what
jQuery.ready(callback)
has been using for ages - it’s triggered after deferred scripts have been defined, giving developers the ability to define mandatory but non-blocking dependencies on top of the page, being sure these will be available once
DOMContentLoaded
is triggered - it’s triggered right before the user sees the page, giving developers the ability to eventually fix CSS quirks on the page
- it’s the best entry point to inject asynchronous modules or libraries, letting the user enjoy the visible content in the meanwhile
There are many others workaround to DOMContentLoaded
, but all are trying to fix a non issue. Having deferred scripts on top, means the browser knows how and when it should download them. Being on top of the page, it means these are so important that nothing else might work for the user so … why would you serve a temporarily broken page to your users?
Having a single entry point where every library can opt in is a very good feature web developers have and I think they should simply use it.
Finally, the best part about defer is that it’s backward compatible, so that if you include an old library as external <script defer src="old-lib.js">
, and such old library contains some DOMContentLoaded
event listener, this will just work without problems: no need to upgrade the Jurassic library … “yaiiiiiii \o/“
What about async
attribute?
I think async
is still a valid attribute to use with any external library you don’t care about, meaning analytics
or other libraries completely invisible and pointless to the final user, you don’t need them to be parsed and executed at any reasonable time during your web app/page lifecycle.
If you dont’ care about “when“, use async
, and also remember that a library that uses a DOMContentLoaded
in a script embedded as async
might never trigger because if such event already triggered, it won’t trigger again … which is a very good reason to indeed use document.ready.then(initMyLib)
for every single third part script you might include as async
in your page.
Moreover, if you have document.ready
available, and you really wanna use async
, you can finally use async
for every single external script you want!
But of course, this is true as long as you’ve polyfilled document.ready
upfront, or until it’s available in your target browsers.
As Summary
Check the live demo so see what discussed in this post works.
You don’t need to mention me or this site to use the script, its related licence is a WTFPL … really!