106

I would like to enable caching of an ajax response in javascript/browser.

From the jquery.ajax docs:

By default, requests are always issued, but the browser may serve results out of its cache. To disallow use of the cached results, set cache to false. To cause the request to report failure if the asset has not been modified since the last request, set ifModified to true.

However, neither of these address forcing caching.

Motivation: I want to put $.ajax({...}) calls in my initialisation functions, some of which request the same url. Sometimes I need to call one of these initialisation functions, sometimes I call several.

So, I want to minimise the requests to the server if that particular url has already been loaded.

I could roll my own solution (with some difficulty!), but I would like to know if there is a standard way of doing this.

cammil
  • 9,499
  • 15
  • 55
  • 89
  • I wouldn't have thought it difficulty to track which URLs you've already loaded and store the results against that list. Then, you can check your URLs before you make an AJAX call. Voila - you have your own basic cache. –  Jun 14 '13 at 08:33
  • you could add the cache-control and expires header to your response on the server, so your server should only be called after the timeout you configured in that values – hereandnow78 Jun 14 '13 at 08:37
  • It's very long, but can you help me understand why you needed cache in ajax requests (possibly in layman terms) – Asish Nov 02 '20 at 09:30
  • It's not clear why the built in browser caching is not good enough? – Martlark Jul 24 '23 at 05:11

6 Answers6

158

cache:true only works with GET and HEAD request.

You could roll your own solution as you said with something along these lines :

var localCache = {
    data: {},
    remove: function (url) {
        delete localCache.data[url];
    },
    exist: function (url) {
        return localCache.data.hasOwnProperty(url) && localCache.data[url] !== null;
    },
    get: function (url) {
        console.log('Getting in cache for url' + url);
        return localCache.data[url];
    },
    set: function (url, cachedData, callback) {
        localCache.remove(url);
        localCache.data[url] = cachedData;
        if ($.isFunction(callback)) callback(cachedData);
    }
};

$(function () {
    var url = '/echo/jsonp/';
    $('#ajaxButton').click(function (e) {
        $.ajax({
            url: url,
            data: {
                test: 'value'
            },
            cache: true,
            beforeSend: function () {
                if (localCache.exist(url)) {
                    doSomething(localCache.get(url));
                    return false;
                }
                return true;
            },
            complete: function (jqXHR, textStatus) {
                localCache.set(url, jqXHR, doSomething);
            }
        });
    });
});

function doSomething(data) {
    console.log(data);
}

Working fiddle here

EDIT: as this post becomes popular, here is an even better answer for those who want to manage timeout cache and you also don't have to bother with all the mess in the $.ajax() as I use $.ajaxPrefilter(). Now just setting {cache: true} is enough to handle the cache correctly :

var localCache = {
    /**
     * timeout for cache in millis
     * @type {number}
     */
    timeout: 30000,
    /** 
     * @type {{_: number, data: {}}}
     **/
    data: {},
    remove: function (url) {
        delete localCache.data[url];
    },
    exist: function (url) {
        return !!localCache.data[url] && ((new Date().getTime() - localCache.data[url]._) < localCache.timeout);
    },
    get: function (url) {
        console.log('Getting in cache for url' + url);
        return localCache.data[url].data;
    },
    set: function (url, cachedData, callback) {
        localCache.remove(url);
        localCache.data[url] = {
            _: new Date().getTime(),
            data: cachedData
        };
        if ($.isFunction(callback)) callback(cachedData);
    }
};

$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
    if (options.cache) {
        var complete = originalOptions.complete || $.noop,
            url = originalOptions.url;
        //remove jQuery cache as we have our own localCache
        options.cache = false;
        options.beforeSend = function () {
            if (localCache.exist(url)) {
                complete(localCache.get(url));
                return false;
            }
            return true;
        };
        options.complete = function (data, textStatus) {
            localCache.set(url, data, complete);
        };
    }
});

$(function () {
    var url = '/echo/jsonp/';
    $('#ajaxButton').click(function (e) {
        $.ajax({
            url: url,
            data: {
                test: 'value'
            },
            cache: true,
            complete: doSomething
        });
    });
});

function doSomething(data) {
    console.log(data);
}

And the fiddle here CAREFUL, not working with $.Deferred

Here is a working but flawed implementation working with deferred:

var localCache = {
    /**
     * timeout for cache in millis
     * @type {number}
     */
    timeout: 30000,
    /** 
     * @type {{_: number, data: {}}}
     **/
    data: {},
    remove: function (url) {
        delete localCache.data[url];
    },
    exist: function (url) {
        return !!localCache.data[url] && ((new Date().getTime() - localCache.data[url]._) < localCache.timeout);
    },
    get: function (url) {
        console.log('Getting in cache for url' + url);
        return localCache.data[url].data;
    },
    set: function (url, cachedData, callback) {
        localCache.remove(url);
        localCache.data[url] = {
            _: new Date().getTime(),
            data: cachedData
        };
        if ($.isFunction(callback)) callback(cachedData);
    }
};

$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
    if (options.cache) {
        //Here is our identifier for the cache. Maybe have a better, safer ID (it depends on the object string representation here) ?
        // on $.ajax call we could also set an ID in originalOptions
        var id = originalOptions.url+ JSON.stringify(originalOptions.data);
        options.cache = false;
        options.beforeSend = function () {
            if (!localCache.exist(id)) {
                jqXHR.promise().done(function (data, textStatus) {
                    localCache.set(id, data);
                });
            }
            return true;
        };

    }
});

$.ajaxTransport("+*", function (options, originalOptions, jqXHR, headers, completeCallback) {

    //same here, careful because options.url has already been through jQuery processing
    var id = originalOptions.url+ JSON.stringify(originalOptions.data);

    options.cache = false;

    if (localCache.exist(id)) {
        return {
            send: function (headers, completeCallback) {
                completeCallback(200, "OK", localCache.get(id));
            },
            abort: function () {
                /* abort code, nothing needed here I guess... */
            }
        };
    }
});

$(function () {
    var url = '/echo/jsonp/';
    $('#ajaxButton').click(function (e) {
        $.ajax({
            url: url,
            data: {
                test: 'value'
            },
            cache: true
        }).done(function (data, status, jq) {
            console.debug({
                data: data,
                status: status,
                jqXHR: jq
            });
        });
    });
});

Fiddle HERE Some issues, our cache ID is dependent of the json2 lib JSON object representation.

Use Console view (F12) or FireBug to view some logs generated by the cache.

TecHunter
  • 6,091
  • 2
  • 30
  • 47
  • is there a reason you put a callback on the `localCache.set` function? Why not simply `doSomehing(jqXHR)` after setting the cache? – cammil Jun 16 '13 at 15:11
  • I just prefer it this way so I don't have to do something like `doSomething(localCache.set(url,jqXHR));` but it's just personnal preference – TecHunter Jun 17 '13 at 06:40
  • 2
    Any suggestion for improving this to support using $.ajax as a promise? Returning false from beforeSend cancels the request (as it should) so existing $.ajax(...).done(function(response) {...}).fail(...) now stop working because fail is invoked rather than done... and I would rather not rewrite them all :( – franck102 Jun 06 '14 at 17:42
  • @franck102 oh yeah haven't thought of that one. I will work on something this week end and update the answer – TecHunter Jun 06 '14 at 22:14
  • 2
    @TecHunter Thanks so much for this. Three more slight improvements could be made. First, if multiple requests are made to the same resource at the same time they will all miss the cache. To fix this you might want to set the cache for a certain "id" to pending with the first request and defer sending results for subsequent requests until the first comes back. Second, you might want to cache the error result of a request so that all requests for the same resource get the same result. – mjhm Oct 15 '14 at 14:08
  • @TecHunter ... Finally (a small quibble) you might want to wrap the "completeCallback" in a setTimeout(..., 0) so that the result is always asynchronous whether it comes from the cache or not. Some bad programs might depend on a certain order of operations, and bugs from results that are sometimes sync and other times async can be really tough to track down. Thanks again for your great answer, I'd upvote it by +10 if I could. – mjhm Oct 15 '14 at 14:17
  • @mjhm thx :) I will just leave it like this. As for returning the same error... Some may want to redo the query in case of an error instead of caching. Nice suggestions though for the other points – TecHunter Oct 15 '14 at 14:24
  • I use your solution, the difference is i use success function in Ajax instead of promise, but i found that completeCallback didn't trigger success function, do you know how could this happen? – Jack Zhang Dec 12 '14 at 02:31
  • @TecHunter, Your 2nd solution works great! However, I need the caching functionality for phonegap app. Which cache method is best to store the JSON data return by the ajax/service call. Is storing JSON data in localCache affect more in browser load/(sometimes phone hangs on) or not? If any best alternative method is there please let me know. Thanks! – immayankmodi Jun 03 '15 at 05:57
  • @MMTac I'm no phonegap expert but in html5 w3c specs you have localStorage which is [supported by phonegap too](http://docs.phonegap.com/en/edge/cordova_storage_localstorage_localstorage.md.html) So instead of using a localCache variable you can use one of the native storage. – TecHunter Jun 03 '15 at 07:52
  • @TecHunter, That's great! Actually I'm aware of it but just needs to confirm which one is best. Thanks for prompting me right choice for it. – immayankmodi Jun 03 '15 at 09:43
  • @TecHunter your second solution is working, but i am using categoreyID instead of url, i want to save cache for multiple list items, when i am using your second solution it is working for first item, when we click on second item ID is not changing and showing first ID data. Ajax request is not processing for second ID. – Devi Prasad Mar 01 '16 at 12:38
  • @DeviPrasad best way to get help is to provide a stackoverflow question :) link it here. don't forget a fiddle – TecHunter Mar 01 '16 at 13:13
  • @TecHunter here is my question http://stackoverflow.com/questions/35716569/how-to-set-caching-for-ajax-request-and-response – Devi Prasad Mar 01 '16 at 13:35
  • 2
    @TecHunter - Nice solution, I used this solution with one important change. If cached object is being modified in other functions it will cause issue, so while setting and getting cached object, I'm returning copy of that object which as shown below: `localCache.data[url] = { _: new Date().getTime(), data: _.cloneDeep(cachedData, true) }; _.cloneDeep(localCache.data[url].data, true)` – Bharat Patil Mar 31 '16 at 04:42
  • Great solution. I tried using it at product filter of a shopping site. It works as long as user doesn't reload the page. Why it doesn't read from cache of reloading entire page? – wp student Jul 29 '16 at 18:07
  • @wpstudent it's because I use runtime variable `localStorage` which will be reset on page change. If you want to use HTML5 localStorage you will need to serialize the data because it stores only strings but it will persists accross pages – TecHunter Jul 29 '16 at 21:02
  • @TecHunter, Thanks for the tip, I'll try modifying it for HTML5 localStorage. – wp student Jul 30 '16 at 05:21
  • @TecHunter when I have used ajax prefilter code above works but not work with datatable callback `oSettings.jqXHR.done(function(json) { fnCallback(json); })` this code fnCallback function not call. What should I do for that Please help. – Nikunj Chotaliya Sep 01 '17 at 08:45
  • @NikunjChotaliya please ask in another SO question, I can't help with at least a fiddle. what is `oSettings` though? I need to see the full prefilter implementation – TecHunter Sep 01 '17 at 09:38
  • @TecHunter https://stackoverflow.com/questions/46000682/callback-in-jquery-ajax-not-working-when-using-jquery-ajax-cache-code-below is the link to my question. – Nikunj Chotaliya Sep 01 '17 at 12:53
  • I think that this solution has a major drawback. It works only while having your browser (more specifically browser tab) open since your localCache is stored in memory with js. However the browser cache, if set like that with the timeout lives much longer. So every first time you open a browser tab you will think that you have nothing in the cache BUT this will already have stored some urls you might need. Correct me if I miss something. Possibly you could store in a cookie the localCahce but again this has drawbacks. – Smalis Sklavos Jul 11 '18 at 08:04
  • @SmalisSklavos the purpose is to cache and control the cache on a page. For persisting the cache then use the `localStorage` from html5 or `indexedDb`. So you are right, if you use in-memory cache with a simple `localCache` javascript object then you are limited. but replace it with any of these and it should be fine. Depends on what you need – TecHunter Jul 11 '18 at 08:26
  • Yes local storage and indexedDB are alternatives. However, if it is critical to know that for sure what you have in your custom cache is consistent, think of the case that somebody clears the localStorage or browser data but keeps the cache, or the opposite: Clear the cache and keep the others. Those two should be kept consistent. – Smalis Sklavos Jul 11 '18 at 08:47
  • @SmalisSklavos yes the caching strategy is important and not discussed here. – TecHunter Jul 11 '18 at 09:43
  • Most of the time APIs return data based on parameters, so it is not sufficient to index cached result only by `URL`. There should be a `URL-Parameter` mixed indexing solution – RezKesh Nov 15 '18 at 21:09
  • @rezKesh true, then you should hash the URL+PARAM(ordered) as key to your cache. Although careful with caching everything it can grow fast in size – TecHunter Nov 16 '18 at 09:52
  • I think it would be nice to have JS library which lets the developer to choose when to cache or not. For example I would cache logged-in user data, I would never cache pages of the report the user wants to see – RezKesh Nov 16 '18 at 13:13
14

I was looking for caching for my phonegap app storage and I found the answer of @TecHunter which is great but done using localCache.

I found and come to know that localStorage is another alternative to cache the data returned by ajax call. So, I created one demo using localStorage which will help others who may want to use localStorage instead of localCache for caching.

Ajax Call:

$.ajax({
    type: "POST",
    dataType: 'json',
    contentType: "application/json; charset=utf-8",
    url: url,
    data: '{"Id":"' + Id + '"}',
    cache: true, //It must "true" if you want to cache else "false"
    //async: false,
    success: function (data) {
        var resData = JSON.parse(data);
        var Info = resData.Info;
        if (Info) {
            customerName = Info.FirstName;
        }
    },
    error: function (xhr, textStatus, error) {
        alert("Error Happened!");
    }
});

To store data into localStorage:

$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
if (options.cache) {
    var success = originalOptions.success || $.noop,
        url = originalOptions.url;

    options.cache = false; //remove jQuery cache as we have our own localStorage
    options.beforeSend = function () {
        if (localStorage.getItem(url)) {
            success(localStorage.getItem(url));
            return false;
        }
        return true;
    };
    options.success = function (data, textStatus) {
        var responseData = JSON.stringify(data.responseJSON);
        localStorage.setItem(url, responseData);
        if ($.isFunction(success)) success(responseJSON); //call back to original ajax call
    };
}
});

If you want to remove localStorage, use following statement wherever you want:

localStorage.removeItem("Info");

Hope it helps others!

immayankmodi
  • 8,210
  • 9
  • 38
  • 55
10

All the modern browsers provides you storage apis. You can use them (localStorage or sessionStorage) to save your data.

All you have to do is after receiving the response store it to browser storage. Then next time you find the same call, search if the response is saved already. If yes, return the response from there; if not make a fresh call.

Smartjax plugin also does similar things; but as your requirement is just saving the call response, you can write your code inside your jQuery ajax success function to save the response. And before making call just check if the response is already saved.

Paul Shan
  • 614
  • 2
  • 8
  • 14
  • I have responses saved in IndexedDB, is there a way to check IndexedDB? Also, if I use Session Storage then is there a way to check if response present or not using jQuery. I can't include any library other than jQuery. Thanks, – Abhishek Aggarwal Mar 28 '19 at 08:19
8

If I understood your question, here is the solution :

    $.ajaxSetup({ cache: true});

and for specific calls

 $.ajax({
        url: ...,
        type: "GET",
        cache: false,           
        ...
    });

If you want opposite (cache for specific calls) you can set false at the beginning and true for specific calls.

Rebse
  • 10,307
  • 2
  • 38
  • 66
spring
  • 760
  • 4
  • 10
2

Old question, but my solution is a bit different.

I was writing a single page web app that was constantly making ajax calls triggered by the user, and to make it even more difficult it required libraries that used methods other than jquery (like dojo, native xhr, etc). I wrote a plugin for one of my own libraries to cache ajax requests as efficiently as possible in a way that would work in all major browsers, regardless of which libraries were being used to make the ajax call.

The solution uses jSQL (written by me - a client-side persistent SQL implementation written in javascript which uses indexeddb and other dom storage methods), and is bundled with another library called XHRCreep (written by me) which is a complete re-write of the native XHR object.

To implement all you need to do is include the plugin in your page, which is here.

There are two options:

jSQL.xhrCache.max_time = 60;

Set the maximum age in minutes. any cached responses that are older than this are re-requested. Default is 1 hour.

jSQL.xhrCache.logging = true;

When set to true, mock XHR calls will be shown in the console for debugging.

You can clear the cache on any given page via

jSQL.tables = {}; jSQL.persist();
I wrestled a bear once.
  • 22,983
  • 19
  • 69
  • 116
  • I can not find your plugin in github and official website.Please update your answer I need your plugin :) I am follower of you in github. Good job ;) @occams-razor – Alican Kablan Dec 21 '18 at 12:21
-1
        function getDatas() {
            let cacheKey = 'memories';

            if (cacheKey in localStorage) {
                let datas = JSON.parse(localStorage.getItem(cacheKey));

                // if expired
                if (datas['expires'] < Date.now()) {
                    localStorage.removeItem(cacheKey);

                    getDatas()
                } else {
                    setDatas(datas);
                }
            } else {
                $.ajax({
                    "dataType": "json",
                    "success": function(datas, textStatus, jqXHR) {
                        let today = new Date();

                        datas['expires'] = today.setDate(today.getDate() + 7) // expires in next 7 days

                        setDatas(datas);

                        localStorage.setItem(cacheKey, JSON.stringify(datas));
                    },
                    "url": "http://localhost/phunsanit/snippets/PHP/json.json_encode.php",
                });
            }
        }

        function setDatas(datas) {
            // display json as text
            $('#datasA').text(JSON.stringify(datas));

            // your code here
           ....

        }

        // call
        getDatas();

enter link description here