View on GitHub

Non Onload Blocking Async JS Loading Evolved

Load 3rd party scripts in parallel with the rest of your content without blocking onload.

The history

Major 3rd parties such as Twitter, Facebook, Disqus and etc are now requiring "Async Pattern" to load their scripts since Google Analytics had launched asynchronous tracking code in 2009.

Async pattern make a site speed fast because it doesn't block parsing and rendering a page. But it still blocks window.onload of the site.

This browser's behavior is reasonable or logically right because such requests to 3rd party scripts come from the page. But from the view point of site design, especially for SEO, this behavior is undesirable if those scripts are not for the main contents.

Therefore, in 2012 Stoyan Stefanov made a simplified version of Meebo's non-onload-blocking embed code, and implemented into facebook JavaScript SDK experientally.

He wrapped the SDK with parent.window if window.inDapIF is true like this:

try {
    window.FB || (function (window) {
        var self = window,
            document = window.document;
        ...
    }).call({}, window.inDapIF ? parent.window : window);
} catch (e) {
    new Image().src = "somthing error image";
}

And we can use this system, which name is "frame-in-frame", at the client side as he showed at JS Bin (or this fiddle):

// frame-in-frame
(function (url) {
    var iframe = document.createElement('iframe');
    (iframe.frameElement || iframe).style.cssText = 'display:none';
    iframe.src = 'javascript:false';
    var where = document.getElementsByTagName('script')[0];
    where.parentNode.insertBefore(iframe, where);
    var doc = iframe.contentWindow.document;
    doc.open().write('<body onload="' +
        'window.inDapIF = true;' +
        'var js = document.createElement(\'script\');' +
        'js.src = \'' + url + '\';' +
        'document.body.appendChild(js);">');
    doc.close();
}('//connect.facebook.net/en_US/all.js'));

// async init once loading is done
window.fbAsyncInit = function() {
    FB.init({xfbml: true});
};

Unfortunately, we can't use this method except for facebook unless the other 3rd parties implement this system into their scripts at their server side.

Solution at client side

I found that we can use similar system at client side. Let's pick up Nicolas Gallagher's snippet.js.

// snippet.js
(function (doc, script) {
    var js,
        fjs = doc.getElementsByTagName(script)[0],
        add = function (url, id) {
            if (doc.getElementById(id)) {return;}
            js = doc.createElement(script);
            js.src = url;
            js.async = true;
            id && (js.id = id);
            fjs.parentNode.insertBefore(js, fjs);
        };

    // Google Analytics
    add(('https:' == location.protocol ? '//ssl' : '//www') + '.google-analytics.com/ga.js', 'ga');
    // Google+ button
    add('https://apis.google.com/js/plusone.js');
    // Facebook SDK
    add('//connect.facebook.net/en_US/all.js#xfbml=1', 'facebook-jssdk');
    // Twitter SDK
    add('//platform.twitter.com/widgets.js', 'twitter-wjs');
}(document, 'script'));

Wrap above code with parent.window and save it as an updated snippet.js:

var win = parent ? parent.window : window;
(function (window, document) {
    // snippet.js here
    ...
}(win, win.document));

Then put the code bellow somewhere near the </body>.

<script>
    var _gaq = _gaq || [];
    _gaq.push(['_setAccount', 'UA-XXXXX-X']);
    _gaq.push(['_trackPageview']);

    (function (url) {
        setTimeout(function () {
            // frame-in-frame here
        }, 0);
    }('snippet.js')); // load snippet.js through "frame-in-frame"
</script>

I added setTimeout(function () {...}, 0) since WebKit (Chrome 29 on Mac) seems to keep the context even with frame-in-frame under some complex situations.

Now we don't need window.inDapIF = true; in our new FIF system. Also we can add a fragment #xfbml=1 to Facebook url instead of calling FB.init(...).

And fortunately, since the variable _gaq is accessed through window._gaq in ga.js , you can move it into snippet.js if you want.

Anyway, using snippet.js, we can now load the 3rd party scripts asynchronously without blocking window.onload that should fire only for the main contents.

Demos

Key point and drawbacks

Fetching 3rd party scripts directly in the FIF system similar to Stoyan's original code fails to get non onload blocking behavior.

doc.open().write('<body onload="' +
    'var win = parent.window;' +
    '(function (window, document) {' +
        // DOM injection here
    '})(win, win.document)' +
    '">');
doc.close();

The key point here is creating a separate file like snippet.js. It means that a page needs to spend additional HTTP request. It also take some additional cost for creating iframe.

And at sometime especially in case of empty cache, a browser loses its non onload blocking behavior. I do not know the reason and solution yet.

Anyway, I think this kind of technique should be applied after some efforts to make fewer HTTP requests.

Rivisiting onload

What's the value of window.onload? Is it only a timing in order to know when to kick up some messy scripts?

Which metric is most important to give a good user experience?

A kind of this question is frequently asked. It's hard to mesure. For example, Google Analytics mesures 8 metrics for site speed to analyze with various perspective, and WebPagetest defines 6 metrics (Speed Index is most interesting) to mesure overoall site speed.

For these metrics, a timing of window.onload is still dominant because it's easy to mesure. For example, GA gets "Page Load Time" from Navigation Timing calculating window.performance.loadEventStart - window.performance.navigationStart which is nearly equal to window.onload.

In these days, onload has been less important than before. But if a site is free from curse of 3rd party scripts which blocks onload, a timing of firing onload will be the best timing for a user to read the main contents, especially if we are concerned about "above the fold". So I think it's good time to focus to onload again as not only a metric but also a break point of a page.

And we have a good chance to claim fair rating for the site speed to Google.

Any discussions ?

Since I'm not a expert of JavaScript nor front end engineering, any discussion well be appreciated. Please open issue.