Ajax Caching, Transports Compatible with jQuery Deferred

squirrelwithnuts

Ever since the advent of memcached and its ilk, the server-side has been able to benefit from reduced load by caching recently or oft-requested resources. This hasn’t become any less important and valuable. If anything, in this era of the webApp, when native application look and feel is increasingly desired, speedy response to requests is critical for your application to meet the needs of your time-pressed users.

Server-side caching can’t save us from the delay that making a roundtrip from the server imposes, however, and if you’re serving hundreds or thousands of users at the same time, even small memory-cached items may, to the user, seem to be taking their sweet time showing up. For small amounts of oft-requested data that isn’t likely to change often, we can eliminate even these delays by shifting the burden client-side, with local storage and ajax request caching.

So, assuming we’re using jQuery, our ajax requests tend to look something like:

$.ajax({
    data:{data:data},
    dataType:'json',
    type:'POST',
    url:'/endpoint'
}).done(function(result){
    // Success! Do something with it.
}).fail(function(jqXHR){
    // Failure! Do something about it.
});

Now, we could start bracketing all our requests with if statements, to dip into our cache and check before we make our request, but this poses (at least) two problems:

  1. If we’re trying to take advantage of jquery Deferred (and we should be), then it messily complicates our $.when to need to have if statements around our ajax request.
  2. We end up needing two success blocks – one inside the if statement (if the value we’re looking for is in the cache), and one in our ajax.done block.

Instead, we want to extend $.ajax so that if the value we want is in the cache, we grab it from there, and if not, we make our request to the server – either way, we can handle the result without needing to write any extra code. This solution by Paul Irish gets us most of the way – adding some options to ajax that allows us to specify if we want to cache the result, what the cacheKey should be, time-to-live, etc. However, it’s designed to work with jQuery’s now deprecated ‘success’ and ‘failure’ callbacks, instead of taking advantage of the promises wrapped up in jqXHR. Changing that requires us to dip our toes into the deep end, with Ajax Transports.

The jQuery docs say it best, but in summary, an ajax transport provides two functions, send and abort, which are used by $.ajax to actually make the request (or, as the name suggests, abort it). Each request has its own transport, so we register a transport factory when we want to override the usual send and abort – such as when we want to draw from the cache, if available. Here’s an example:

The important thing to remember about $.ajaxTransport is that if we return something, that’s considered the transport that should be used. If you return without conditions, that transport will be used for all ajax requests – not generally what we want. Instead, we specify conditions and return something only if the conditions are met, which will cause the standard transport to be overridden with our specified one.

In the above example, you can see we’re specifying ‘json’ before our factory function – this is the first limit on the factory, indicating that we should only even consider overriding the transport for ajax requests with dataType:’json’. Within the factory function, we have further checks against the cache and only if a value is found for the given (or generated, in line with Paul Irish’s plugin) cacheKey do we override the transport to return our value. By leaving out the ‘json’ specifier (or replacing it with, say, ‘script’ or ‘html’) we can change what will be considered a valid target for checking against the cache.

Also note the use of storage – this is just a simple global variable set to work with sessionStorage preferably, localStorage or, as a last-ditch effort, an object:

// We cache to sessionStorage (first choice), localStorage (second choice) or an object (last resort).
var storage = (typeof(sessionStorage) == undefined) ?
    (typeof(localStorage) == undefined) ? {
        getItem: function(key){
            return this.store[key];
        },
        setItem: function(key, value){
            this.store[key] = value;
        },
        removeItem: function(key){
            delete this.store[key];
        },
        clear: function(){
            for (var key in this.store)
            {
                if (this.store.hasOwnProperty(key)) delete this.store[key];
            }
        },
        store:{}
    } : localStorage : sessionStorage;

You can ignore or adapt this to your own purpose, either replacing storage with localStorage explicitly (as per the original plugin) or else subbing with another plugin that offers cross-browser local storage functionality (like one of these).

Finally, here’s a (very lightly) modified version of the original plugin to work with our new transport in providing cacheable ajax queries, with thanks again to Paul Irish.

For the moment, if there’s a cache error (such as you’ve hit your limit), the new item fails to be stored. More advanced behaviour is left as an exercise to the reader.

Christopher Keefer

Christopher Keefer

Christopher Keefer is a Senior Software Engineer at Art & Logic. He generally spends his spare time on the computer too, so there isn't much hope for him.
Christopher Keefer

Latest posts by Christopher Keefer (see all)

Tags:

Creative Commons License

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

4 Comments

  1. dobesv

    I wonder how this compares to using http caching headers and letting the browser cache it for you. What do you think?

    • sanemethod

      If you have control of the server response, by all means, let the browser do the work of caching for you where appropriate. If you’re making calls out to APIs not directly under your control (not uncommon), then ajax caching of this sort is the next-best solution (less clumsy than server proxies, I feel).

      It also buffers you from the sometimes inconsistent caching behaviour of the various browsers (older versions in particular), and allows you to cache results for specific requests when you have otherwise disabled caching (by, say, adding cache:false to your $.ajaxSetup).

  2. Charley

    Thanks for you post, it was useful for me, and I found a bug in my usage, the options.type in ajaxPrefilter maybe was LowerCase, I simple fix it by convert all options.type to UpperCase one.

  3. Jeremy

    Very interesting post. Exactly what I have searched for. The original script from Paul Irish was licensed under Apache License, Version 2.0. Do you have any license that your script can be used with? Otherwise it would be pretty useless…

Trackbacks/Pingbacks

  1. jQuery Ajax Blobs and Array Buffers | - […] we’re going to borrow a trick we discussed back in our Ajax Caching article, and create an Ajax Transport…