Non-onload-blocking async JS

Asynchronous JS is cool but it still blocks window.onload event (except in IE before 10). That's rarely a problem, because window.onload is increasingly less important, but still...

At my Velocity conference talk today Philip "Log Normal" Tellis asked if there was a way to load async JS without blocking onload. I said I don't know, which in retrospect was duh! because I spoke about Meebo's non-onload-blocking frames (without providing details) earlier in the talk.

Stage fright I guess.

Minutes later in a moment of clarity I figured Meebo's way should help. Unfortunately all Meebo docs are gone from their site, but we still have their Velocity talk from earlier years (PPT). There are missing pieces there but I was able to reconstruct a snippet that should load a JavaScript asynchronously without blocking onload.

Here it goes:

(function(url){
  var iframe = document.createElement('iframe');
  (iframe.frameElement || iframe).style.cssText = "width: 0; height: 0; border: 0";
  var where = document.getElementsByTagName('script');
  where = where[where.length - 1];
  where.parentNode.insertBefore(iframe, where);
  var doc = iframe.contentWindow.document;
  doc.open().write('<body onload="'+
    'var js = document.createElement(\'script\');'+
    'js.src = \''+ url +'\';'+
    'document.body.appendChild(js);">');
  doc.close();
})('http://www.jspatterns.com/files/meebo/asyncjs1.php');

The demo page is right here. It loads a script (asyncjs1.php) that is intentionally delayed for 5 seconds.

Features

  • loads a javascript file asynchronously
  • doesn't block window.onload nor DOMContentLoaded
  • works in Safari, Chrome, Firefox, IE6789 *
  • works even when the script is hosted on a different domain (third party, CDN, etc), so no x-domain issues.
  • no loading indicators, the page looks done and whenever the script arrives, it arrives and does its thing silently in the background. Good boy!

* The script works fine in Opera too, but blocks onload. Opera is weird here. Even regular async scripts block DOMContentLoaded which is a shame.

Drawback

The script (asyncjs1.php) runs is in an iframe, so all document and window references point to the iframe, not the host page.

There's an easy solution for that without changing the whole script. Just wrap it in an immediate function and pass the document object the script expects:

(function(document){
 
  document.getElementById('r')... // all fine
 
})(parent.document);

How does it work

  1. create an iframe without setting src to a new URL. This fires onload of the iframe immediately and the whole thing is completely out of the way
  2. style the iframe to make it invisible
  3. get the last script tag so far, which is the snippet itself. This is in order to glue the iframe to the snippet that includes it.
  4. insert the iframe into the page
  5. get a handle to the document object of the iframe
  6. write some HTML into that iframe document
  7. this HTML includes the desired script

This entry was posted on Thursday, June 28th, 2012 and is filed under JavaScript, performance. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.


Get notification for future posts: follow me on Twitter or subscribe to my RSS feed

24 Responses to “Non-onload-blocking async JS”

  1. HB Says:

    Nice, thanks for piecing this together, I bet Meebo has a lot of cool hacks that may be disappearing along with their products =(

    One question, since your talk was mainly about dealing with third party scripts. Since you might not control the target script, how do you adapt document to parent.document? I guess you could proxy and wrap the script via a PHP page you DO control, but is there a way to just load the third party script directly without messing up uses of window/document?

  2. Marcel Duran Says:

    This is exactly how YSlow bookmarklet works plus it sandboxes YUI into it: http://www.yuiblog.com/blog/2011/07/18/next-gen-yslow-powered-by-yui/

    Kudos to Caridy who was inspired by Meebo’s technique and helped me finding the best solution for injecting YSlow as a bookmarklet seamlessly.

  3. Marcel Duran Says:

    Sorry my typo above, the correct link to Caridy’s Twitter is http://twitter.com/caridy.
    * feature request: edit after posting comments

  4. David Murdoch Says:

    I started typing up some what I thought were improvements to this and it started getting a bit long.

    So I’ve posted it on my own blog instead: http://blog.vervestudios.co/blog/post/2012/06/28/Non-onload-blocking-Async-JS-with-requirejs.aspx

  5. Matt Pizzimenti Says:

    Thanks for re-surfacing Meebo’s loading technique again! It’s kind of amazing how cutting-edge that work was (they built it way back in 2010). I think their original code is here:

    https://github.com/meebo/embed-code

    …but I actually like the simplicity of your reconstructed snippet better. After a quick look though, there are a couple tweaks I noticed you might want to add:

    1. Avoid SSL warnings: iframe.src defaults to “about:blank” in IE6, which it then treats as insecure content on HTTPS pages. We found that initializing iframe.src to “javascript:false” fixes this up :)
    2. Avoid crossdomain exceptions: anonymous iframe access will throw exceptions if the host page changed the document.domain value in IE. The original Meebo code falls back to a “javascript:” URL when this happens.

    At Olark, we use the Meebo technique (plus some enhancements) for distributing our own embed code. The implementation has worked really well in the field:

    http://www.olark.com/spw/2011/10/lightningjs-safe-fast-and-asynchronous-third-party-javascript

    It would *awesome* to shrink down the size, while still handling those corner cases and ensuring forward-compatibility. I always find myself wishing for a better way :)

    @HB: I think proxying might be the only way to wrap existing scripts outside your control. This was a common question after our blog post too. I think it mostly comes down to 3rd parties adopting this pattern over time.

  6. Stoyan Says:

    Thanks @David and @Marcel!

    @HB, good question, hmm. Unfortunatelly this snippet requires changes to the script being loaded, even if it’s just a closure around the whole thing passing document, window, etc

  7. Stoyan Says:

    @Matt, thanks for the pointers! Ben Vinegar of Disqus pointed last night to your lightningjs, it’s really cool. Just a tad long, but hey, browsers are messy :)

    Agree, javascript:false is an easy fix, this is also what Facebook SDK does

    #2 issue is really annoying and adding more code for this edge case

    and here’s YSlow’s bookmarklet code (as Marcel mentioned above) in a fiddle: http://jsfiddle.net/J3uaK/ some clever tricks for js golfing there

  8. Warren Gaebel Says:

    Hi, Stoyan. I’ve always assumed that an asynchronous script could be loaded and executed without blocking onLoad simply by letting onLoad trigger it. Would this not work?

  9. Raja Bhogi Says:

    I have been looking for this technique since long time. Thanks for the post.

  10. Stoyan Says:

    @Warren, yes, anything you do like:

    window.onload = function(){/* stuffs */};

    is obviously not going to block `onload`

  11. Warren Gaebel Says:

    Hi, again, Stoyan.

    If the simple one-liner you provided in your previous comment will solve the problem, why then would we use the 13-liner you propose in the article? It seems like extra work and extra processing time. I’m sure you’re seeing some benefit that I am missing. Would you please bring me up to speed? Thanks bunches.

  12. Philip Tellis Says:

    @Warren the one liner doesn’t solve the real problem, it just solves one problem and creates a new problem.

    To define the problem correctly, “How do you download scripts _in parallel_ with the rest of your content without blocking onload”. The _in parallel_ part is important. If you defer downloading of the script until after onload, then the total time to download assets increases while network throughput is reduced. The long script gets you the best of both worlds: parallelization + non-blocking.

    As to why this came up… Stoyan wrote this in response to my question at Velocity, and my script can absolutely not load after onload fires because it needs to measure the time when onload fires. It is acceptable for the script to finish loading after onload if the script is too slow, but in the majority case, we need to make sure the script completes loading before onload fires.

  13. Raja Bhogi Says:

    @Warren, By moving the call to on load would increase the overall load time of the page. Whole idea of making non blocking is start the call early without affecting window on load.

  14. Community News: A Journey into DevOps | New Relic blog Says:

    [...] 3 shows how they incorporated New Relic into the Web Fabric service.* Stoyan Stefanov shows how to load JavaScript asynchronously without blocking onload.No comments yetLeave a Reply Cancel replyYour email address will not be [...]

  15. Olga Says:

    May be someone has mentioned this already, but adding a setTimeout( ,10); in iframe seems to fix the problem for Opera browser. We are struggling to support Opera in our company =)
    var requestLink = ‘foo.bar.baz/script.js’;
    loader.open();
    loader.write(”);
    loader.close();

  16. Olga Says:

    Oops, something happend with include code snippet. A link to test page than:

    http://banners.adfox.ru/120808/adfox/215374/test3.Stoyan.html

  17. Pablo Says:

    Hi

    I’m working in a little tiny loader for javascript sources using CORS and make the web more fast !

    The goal is load multiples js in parallel and execute in order without blocking DOMReady or onload.

    https://github.com/pablomoretti/jcors-loader

  18. how to mask my ip address for free Says:

    Advanced Application Support works with all major browsers and dozens of instant messengers, E-mail clients, games, and more!

  19. Philip Tellis (@bluesmoon) Says:

    BTW, is there an easy way to detect that this iframe that you create was created to run the script and not just a regular iframe that the script happens to be inside?

    I find that location.href inside the iframe actually holds the URL of the parent page (maybe to get around cross-domain issues).

  20. Performance Calendar » SPOF: How we fixed a weird bug causing random users’ browsers to freeze Says:

    [...] Load JS in iframe (acc. to Stoyan/Meebo’s approach) [...]

  21. Troy Eckhoff Says:

    Hi! I know this is kind of off topic but I was wondering which blog platform are you using for this website? I’m getting sick and tired of WordPress because I’ve had issues with hackers and I’m looking at options for another platform. I would be fantastic if you could point me in the direction of a good platform.|

  22. Performance Calendar » The non-blocking script loader pattern Says:

    [...] Non-onload-blocking async JS by Stoyan Stefanov [...]

  23. Vic Tsao Says:

    Hi! Stoyan, is the tag with async attribute still blocking `onload` ?
    sorry my english is not good.

  24. Joseph Ishak Says:

    Just came up with this solution a few days ago:

    https://gist.github.com/jishak/5198361#file-async-iframe-demo-html

Leave a Reply