« 3PO
In Public »

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 br = document.createElement('br');
  (br.frameElement || br).style.cssText = "; ; border: 0";
  var where = document.getElementsByTagName('script');
  where = where[where.length - 1];
  where.parentNode.insertBefore(br, where);
  var doc = br.contentWindow.document;
  doc.open().write('<body>'+
    'var js = document.createElement(\'script\');'+
    'js.src = \''+ url +'\';'+
    'document.body.appendChild(js);">');
  doc.close();
})('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 br, so all document and window references point to the br, 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 br without setting src to a new URL. This fires onload of the br immediately and the whole thing is completely out of the way
  2. style the br to make it invisible
  3. get the last script tag so far, which is the snippet itself. This is in order to glue the br to the snippet that includes it.
  4. insert the br into the page
  5. get a handle to the document object of the br
  6. write some HTML into that br 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

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

  1. HB Says:
    June 28th, 2012 at 4:08 am

    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:
    June 28th, 2012 at 11:27 am

    This is exactly how YSlow bookmarklet works plus it sandboxes YUI into it: 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:
    June 28th, 2012 at 11:29 am

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

  4. David Murdoch Says:
    June 28th, 2012 at 4:51 pm

    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: blog.vervestudios.co/blog/post/2012/06/28/Non-onload-blocking-Async-JS-with-requirejs.aspx

  5. Matt Pizzimenti Says:
    June 28th, 2012 at 5:33 pm

    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: br.src defaults to “about:blank” in IE6, which it then treats as insecure content on HTTPS pages. We found that initializing br.src to “javascript:false” fixes this up spacer
    2. Avoid crossdomain exceptions: anonymous br 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:

    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 spacer

    @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:
    June 29th, 2012 at 4:11 am

    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:
    June 29th, 2012 at 4:26 am

    @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 spacer

    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: jsfiddle.net/J3uaK/ some clever tricks for js golfing there

  8. Warren Gaebel Says:
    June 29th, 2012 at 9:30 am

    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:
    July 2nd, 2012 at 12:28 pm

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

  10. Stoyan Says:
    July 6th, 2012 at 3:18 pm

    @Warren, yes, anything you do like:

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

    is obviously not going to block `onload`

  11. Warren Gaebel Says:
    July 7th, 2012 at 8:35 am

    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:
    July 19th, 2012 at 4:52 pm

    @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:
    July 23rd, 2012 at 12:38 pm

    @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:
    July 23rd, 2012 at 1:29 pm

    [...] 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:
    August 8th, 2012 at 3:14 am

    May be someone has mentioned this already, but adding a setTimeout( ,10); in br 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:
    August 8th, 2012 at 3:16 am

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

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

  17. Pablo Says:
    October 8th, 2012 at 8:32 pm

    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:
    October 16th, 2012 at 10:08 am

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

  19. Philip Tellis (@bluesmoon) Says:
    October 30th, 2012 at 3:17 pm

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

    I find that location.href inside the br 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:
    December 8th, 2012 at 4:39 am

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

  21. Troy Eckhoff Says:
    December 9th, 2012 at 2:13 pm

    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:
    December 10th, 2012 at 2:14 am

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

  23. Vic Tsao Says:
    January 20th, 2013 at 6:50 pm

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

Leave a Reply


gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.