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:

document.ready = new Promise(r => /^loaded|complete$/.test(document.readyState) ? r() : document.addEventListener('DOMContentLoaded', r));

— Andrea Giammarchi (@WebReflection) May 9, 2016

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:

document.ready=new Promise(r=>document.addEventListener('DOMContentLoaded',r)) on top of your page and no need to debate about readyState.

— Andrea Giammarchi (@WebReflection) May 10, 2016

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:

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!

Andrea Giammarchi

Fullstack Web Developer, Senior Software Engineer, Team Leader, Architect, Node.js, JavaScript, HTML5, IoT, Trainer, Publisher, Technical Editor