Quantcast
Channel: o2.js » Widget
Viewing all articles
Browse latest Browse all 6

JavaScript Widget Development Best Practices (Part 3: Cache Revalidation)

$
0
0
setting a far future expires header and cache revalidation makes your widget load faster

setting a far future expires header and cache revalidation makes your widget load faster

In the former post we have outlined a broad brushstrokes initialization flow of our external JavaScript Widget:

What we did was to basically create a bootloader script that first loaded required resources asynchronously and then continued its flow with the initialization and rendering of the widget.

Currently, those initialization and rendering functions are empty placeholder functions that are yet to be implemented. We will be dealing with them in the upcoming articles.

For the time being, let’s focus our attention to a more important problem:

Versioning and Caching

For any code that you constantly upgrade an develop versioning and caching are two important things to keep in mind and to get working from day zero, if your end result will be consumed through the Internet.

For our external JavaScript Widget to be responsive, and to load faster it’s a good practice to serve the widget script with a far future expires header.

“For static components: implement “Never expire” policy by setting far future Expires header

A first-time visitor to your page may have to make several HTTP requests, but by using the Expires header you make those components cacheable. This avoids unnecessary HTTP requests on subsequent page views.

Expires headers are most often used with images, but they should be used on all components including scripts, stylesheets, and Flash components.

Browsers (and proxies) use a cache to reduce the number and size of HTTP requests, making web pages load faster.”

It seems, however, that the big guys have not done their homeworks well:

  • google‘s ga.js has a 2 hour cache time;
  • Facebook‘s all.js has a 15 minute cache time;
  • And twitter‘s widgets.js has a 30 minute cache time.

These have negative impact on these scripts’ loading performance.

Ideally they should have a far future expiration date, say… like 10 years;
but heck, even 1 week will be a good starting point ;) .

And There’s a Cache…

However there’s one complication with setting a far future expires header:

If you change your widget code, the users may still be using older versions of your widget.

One approach to solve this is to give your script a version number, and change the version number of the script every time you make a code update and ask the publishers (hint: not a good idea ;) ) to update their widget loader script to reflect the change, like in the following example.

// Widget Include Code
(function() {
    var script = document.createElement('script');
    script.type  = 'text/javascript';
    script.async = true;

    // Ask the publishers to update this version number
    // every time you make a change.
    // This is not the most ideal approach since most
    // of the publishers will rarely want to change a
    // working code snippet that does not belong to them.
    script.src = 'http://api.widget.www/api.v.0.1.js'

    var node = document.getElementsByTagName('script')[0];
    node.parentNode.insertBefore(script, node);
}());

Moreover, assume that what you did is a critical security update. You cannot take it for granted that all the publishers will increment the API version in their initialization scripts.

From former experience, I can say that some most of the publishers will not bother changing that version number and will continue using the older version of the widget.

Cache Revalidation to the Rescue

We will approach the problem as follows:

  • We will create a cache revalidation mechanism to push hotfixes, bugfixes, improvements and non-breaking changes to the current version of the API.
  • And we will copy our script and create a new version (like api.v.0.2.js) for major improvements, and breaking changes.

Doing the latter is as easy as copying the current working file, and renaming it to give a new version number. However, implementing a cache revalidation mechanism is a bit tricky. Let’s try to work on it step by step:

Add a Far Future Expires Header

First start with setting a very long expires header for our API bootloader:

//api.widget.wwwroot/index.js

var express = require('express');
var app     = express.createServer();

var kOneYear = 31536000000;

/*
 * Make sure that the static assets have a far future expiration date.
 */
app.use(
    express['static'](
       path.STATIC_FILE,
       config.farFutureExpiration
    )
)

//where config is:
var config = {
    farFutureExpiration : {maxAge : kOneYear},

    api_v_0_1 : {
        VERSION_TIMESTAMP : '20120720135547909116'
    }
};

Cache Me, If You Can…

We will come to what VERSION_TIMESTAMP is in a minute ;) .

When we request http://api.widget.www/api.v.0.1.js, the HTTP response headers we get are as follows:

Accept-Ranges   bytes
Cache-Control   public, max-age=31536000
Connection      keep-alive
Content-Length  6672
Content-Type    application/javascript
Date            Fri, 20 Jul 2012 20:41:55 GMT
Etag            "6672-1342801807000"
Last-Modified   Fri, 20 Jul 2012 16:30:07 GMT
X-Powered-By    Express

Where

Cache-Control   public, max-age=31536000

indicates that our bootloader script (i.e. api.v.0.1.js) will be cached for 1 year.

If we refresh the webpage and analyze the browser’s cache, we’ll see that it’s in deed true:

Cache:
Data Size       6672
Device          disk
Expires         Sat Jul 20 2013 23:43:10 GMT+0300 (EEST)
Fetch Count     4
Last Fetched    Fri Jul 20 2012 23:43:11 GMT+0300 (EEST)
Last Modified   Fri Jul 20 2012 23:43:10 GMT+0300 (EEST)

Upon that request we will also get a HTTP 304 Not Modified response from the server as expected.

Caching is an indispensable reality of the web which not only widget providers, but every web developer should know. You can read this caching tutorial for caching best practices, and RFC-2616 for HTTP status codes and header definitions.

Self-Updating Bootloader

Now it’s time to implement a self updating bootloader script:

To begin, let us add a version timestamp to the API first:

//api.v.0.1.js

(function(window, document, isDebugMode) {
    ...

    /*
     * Should match beacon version timestamp.
     */
    var versionTimestamp = '20120720135547909116';

    ...

And then pass along that version timestamp to load a cache validator beacon:

    /*
     * Revalidates cache for this bootloader script, if there's a newer
     * version available. The changes will take effect only AFTER the user
     * refreshes the page.
     */
    function checkForUpdates() {
        log('o->checkForUpdates()');

        insertScript(kApiRoot, [kBeacon, kQuery,
            kVersion,  kEquals, versionTimestamp , kAnd,
            kRandom,   kEquals, (new Date()).getTime()
        ].join(kEmpty), noop);
    }

    //Where the constants above (and some more) are defined as follows:

    var kAnd            = '&';
    var kApiRoot        = 'http://api.widget.www/';
    var kBeacon         = 'api/v.0.1/beacon';
    var kCompleteRegExp = /loaded|complete/;
    var kEmpty          = '';
    var kEquals         = '=';
    var kHead           = 'head';
    var kO2Root         = 'http://api.widget.www/lib/o2.js/';
    var kQuery          = '?';
    var kRandom         = 'r';
    var kScript         = 'script';
    var kScriptType     = 'text/javascript';
    var kVersion        = 'v';

In the above code, we basically call:

    insertScript(
        'http://api.widget.www/api/v.0.1/beacon?v=20120720135547909116&r={#}'
    );

Where insertScript simply does what its name implies:

    /*
     * Asynchronously inserts a script element to the head
     * of the document.
     */
    function insertScript(root, src) {
        var s = document.createElement(kScript);
        var x = document.getElementsByTagName(kScript)[0] ||
            document.getElementsByTagName(kHead)[0];

        s.type  = kScriptType;
        s.async = true;
        s.src   = [root, src].join(kEmpty);

        x.parentNode.insertBefore(s, x);

        return s;
    }

Thus, what we do is to request a beacon script at each widget load.

The Cache Validator Beacon

Now let us look at what the beacon script does:

//api.widget.wwwroot/index.js

    ...

    /**
     * Beacon to validate the cache of the JS API.
     */
    app.get(v_0_1(route).BEACON, function(req, res) {
        res.header('Content-Type', 'text/javascript');

        setShortExpiresHeader(res);

        var versionTimestamp = v_0_1(config).VERSION_TIMESTAMP;

        var requestedVersion = req.param(
            parameter.VERSION,
            versionTimestamp
        );

        if (requestedVersion !== versionTimestamp) {
            res.render(template.UPDATE_SCRIPT, {
                iframeUrl : v_0_1(url).UPDATE_IFRAME
            });

            return;
        }

        res.send(204);
    });

If the requested version timestamp is identical to the what we sent, then it means that we are using the most up-to-date bootloader script, we don’t need to update the cache; and the beacon simply sends a HTTP 204 (no content) response.

Otherwise (i.e. if the versions don’t match) then we conclude that the user’s cache is stale and we inject a simple JavaScript in the beacon as follows:

(function() {
    var update = function() {
        if (!document.body) {
            setTimeout(update, 500);
            return;
        }

        var loader = document.createElement('iframe');

        loader.style.display = 'none';
        loader.src = '#{iframeUrl}';

        document.body.appendChild(loader);
    };

     update();
})();

Where we append a hidden IFRAME element to the document.

And the contents of that IFRAME will be (in Jade syntax):

    !!! 5
    html(lang='en')
        head
            script(src= apiBootstrapUrl)
        body(lang='en')
            script
                if (!window.location.hash) {
                    window.location.hash = 'checked';
                    window.location.reload(true);
                }

So the hidden iframe simply reloads the api bootloader script once more, where the true parameter forces the newest script to be retrieved from the server. Which will update the browser’s cache.

Note that the changes will be effective upon the next retrieval of the script:

The most up-to-date version of the script will be served to the end user once she reloads the publisher’s website, or navigates to another page.

This concludes our cache revalidation flow.

Widget Sanity Check

Another issue that you may coincide as a widget developer is that the publisher may unintentionally include your widget bootloader script more than once, or worse she can include several different versions of your widget bootloader script in different parts of the page.

To avoid redefining of resources a sanity check on the top of the widget is required.

//api.v.0.1.js

...

// To avoid re-defining everything if the bootloader is included in
// more than one place in the publisher's website.
if (window._wd) {
    return;
}

window._wd = {};

As always, we do our best to keep the global namespace clean. Note that window._wd is the only accessible object outside the module.

Our assumption here is that, nobody other than the widget provider uses the window._wd namespace, and window._wd namespace solely belongs to the widget provider.

So if a publisher redefines window._wd at their own peril, they should know that our widget will not work properly.

Later, in the upcoming tutorials, it will be our widget namespace that we will export (or expose) public methods of the module to the outside world.

Load Prerequisites

Another thing we would do is to load required assets (such as helper libraries) before continuing with the widget’s initialization flow.

We do this in loadPrerequisites function as follows:

    /*
     * Load necessary o2.js components in noConflict mode.
     */
    function loadPrerequisites(callback) {
        loadScripts(kO2Root, [
            'o2.meta.js',
            'o2.core.js'
        ], callback);
    }

Where loadScripts is:

    /*
     * Loads an array of scripts one after another.
     */
    function loadScripts(root, ar, callback) {
        // Populate global script queue.
        scriptQueue = ar;

        loadScript(root, scriptQueue.shift(), callback);
    }

And loadScript is:

    /*
     * Loads the given script.
     * <strong>callback</strong> is the function to be executed after
     * there's no resource left to be loeded next.
     */
    var loadScript = function(root, src, callback) {
        var s = insertScript(root, src);

        function processNext() {
            loadNext(root, loadScript, callback);
        }

        s.onreadystatechange = function() {
            if(kCompleteRegExp.test(s.readyState)) {
                processNext();
            }
        };

        s.onload = function() {
            processNext();
        };
    };

So we inject an array of script one after another until all of the scripts are loaded and then execute a callback.

This callback continues our widget initialization flow, which is the subject of the next article in this series.

Read the Source Luke

You can download the source code of what we’ve done so far from this github history snapshot.

Conclusion

In this article we’ve built upon what we’ve learned last time.

  • We’ve implemented a cache validator beacon, that refreshes the cache whenever there’s a new version of the script;
  • We’ve implemented a way to prevent multiple widget initialization if the script is included more than once on the page;
  • We’ve also implemented a mechanism to preload required assets before continuing with the widget initialization flow.

In the upcoming article, we will continue our journey with initializing and rendering our widget.

Until then, feel free to share your comments an suggestions.


Viewing all articles
Browse latest Browse all 6

Latest Images

Trending Articles





Latest Images