basket.js logo

A simple (proof-of-concept) script loader that caches scripts with localStorage

Tweet

Version 0.5.0: basket.js (2.2 kB)   basket.min.js* (0.7 kB gzipped)   basket.full.min.js (3.8 kB gzipped)   * = Use this version if you want to manually handle the rsvp.js dependency

If you're seeing this message, something has gone wrong. Please ensure you are testing this page using a HTTP server.

Introduction

basket.js is a small JavaScript library supporting localStorage caching of scripts. If script(s) have previously been saved locally, they will simply be loaded and injected into the current document. If not, they will be XHR'd in for use right away, but cached so that no additional loads are required in the future.

Why localStorage?

Tests by Google and Bing have shown that there are performance benefits to caching assets in localStorage (especially on mobile) when compared to simply reading and writing from the standard browser cache. This project is currently working on replicating these tests in the public so that we have definitive statistics on whether this is true.

Developers have also been wondering why we opted for localStorage as opposed to alternatives such as IndexedDB. Jens Arps has shown that IDB is at present significantly slower than localStorage for reading and writing assets. Other developers exploring this space have also shown that localStorage works just fine for caching data (it's actually significantly faster in Safari at the moment than any other browser).

We believe that in time, once implementers have optimized localStorage, it will become more feasible to use cross-browser. In the mean time, we are also exploring solutions such as the FileSystem API as a storage mechanism for caching scripts locally.

Project History

This project was created as a response to a tweet by Steve Souders asking that the jQuery project consider caching jQuery in localStorage for performance reasons. Intrigued by the idea, we began exploring a minimalistic solution to it (just for the sake of experimentation).

You may remember Souders previously did research and a write-up on search engines making use of localStorage caching back in 2011. His conclusions were:

Bing and Google Search make extensive use of localStorage for stashing SCRIPT blocks that are used on subsequent page views. None of the other top sites from my previous post use localStorage in this way. Are Bing and Google Search onto something? Yes, definitely.

To help provide a more general solution for this problem, we put together a script loader that treats localStorage as a cache.

Compatibility

basket.js supports locally caching scripts in any browser with localStorage capabilities.

About localStorage

localStorage is a simple API within modern browsers to allow web developers to store small amounts of data within the user's browser.

The HTML5 spec suggests storage quota with a limit of 5MB for localStorage but browsers can implement their own quota if they wish. If the quota is exceeded the browser may fail to store items in the cache. If this happens, basket.js will remove entries from the cache beginning with the oldest and try again. Some browsers like Opera will ask the user about increasing the quota when it exceeds a set threshold.

To free space basket.js will only remove cached scripts that it placed in localStorage itself. The data stored by other code in same origin will not be touched.

API

basket.require

basket.require(details)

details: Either an object or an array of objects with the following fields:

require() returns a promise that will be fulfilled when each of the requested items has been fetched, or rejected if any item fails.

Examples

Single script

basket.require({ url: 'jquery.js' });

This fetches a single script and executes it. If the script was already in the localStorage cache it will be loaded from there, otherwise it will be loaded from the network. The script will then be injected into the document for the browser to execute.

Multiple scripts

basket.require(
    { url: 'jquery.js' },
    { url: 'underscore.js' },
    { url: 'backbone.js' }
);

Multiple scripts will be requested. The scripts are requested asynchronously but executed in the same order as specified.

Multiple scripts without caching some of them

basket.require(
    { url: 'require.js' },
    { url: 'require.config.js', skipCache: true },
    { url: 'libs.js' }
);

Multiple scripts will be requested. require.config.js will not be cached in localStorage. Useful if order of scripts execution is important but storing certain script is not needed, e.g. it changes with each request.

Ordering dependencies

basket
    .require({ url: 'jquery.js' })
    .then(function () {
        basket.require({ url: 'jquery-ui.js' });
    });

Here we ask basket.js to load jQuery. Once it has been fetched and executed, the promise returned by require() will be fulfilled and the callback passed to the then() method of the promise will be executed. Now we can do anything the requires jquery to be loaded including load any scripts that depend on it.

Error handling

basket
    .require({ url: 'missing.js' })
    .then(function () {
        // Success
    }, function (error) {
        // There was an error fetching the script
        console.log(error);
    });

The second parameter to then() is a function that will be called if the promise is rejected. That is, if there was an error fetching the script. The only parameter to the error callback will be an Error object with details of the failure.

Using an alias

basket.require({ url: 'jquery-2.0.0.min.js', key: 'jquery' });

If you wish to store a script under a specific key name (e.g. if you have a build process which creates a script with a name like 012345.js and want to store it as, say, main), you can set the key property to the name you want to use.

This can also be useful for libraries with version numbers in their URIs when you don't need a particular version. In the above example the cache will be checked for a script stored as "jquery" regardless of its original URI. This allows us to use an older version stored in the cache if one exists.

If key is not set the url will be used instead.

Cache expiry

basket.require({ url: 'jquery.min.js', expire: 2 })

Here script will only be cached for up to 2 hours. After that time it will be fetched from the network again even if it exists in the cache. To re-fetch after 7 days you could set 168 ( i.e. 7 * 24 = 168 ).

If expire is not set, the default time of 5000 hours will be used - almost 7 months.

Cache a file without executing it

basket.require({ url: 'jquery.min.js', execute: false });

The script will be cached but will not be added to the document to be executed.

Cache busting

// fetch and cache the file
basket.require({ url: 'jquery.min.js' });
// fetch and cache again
basket.require({ url: 'jquery.min.js', unique: 123 });

Set the unique property to control whether the script will be loaded from the cache. If the parameter is different in the request to that stored in the cache the file is fetched again.

basket.js will add the "basket-unique" parameter on to the url to also prevent the script being fetched from the browser cache.

basket.get

basket.get(key)

key The key to lookup in the localStorage cache.

get() will return an object if script is found or false if not. The object contains the same data as that passed to require() when it was first loaded, with some additional details added:

var req
var ttl;

basket.require({ url: 'jquery.min.js', key: 'jquery' });
req = basket.get('jquery');
// know the lifetime
ttl = req.expire - req.stamp;

basket.remove

basket.remove(key)

key The key to remove from the localStorage cache.

remove() will simply remove a previously cached script from localStorage. An example of how to use it can be seen below:

basket
    .remove('jquery.js')
    .remove('modernizr');

basket.clear

basket.clear(expired)

expired If expired is true then only items that have expired will be removed. Otherwise all items are removed.

clear() removes items from the cache.

basket.clear();
basket.clear(true);

basket.isValidItem

basket.isValidItem(source, obj)

Optional This property can be set to a custom validation method that should return a boolean value depending on the validity of the item (source). isValidItem is called for each item in a require call.

isValidItem() is expected to return true is the item is valid. If isValidItem() returns false, the item will be loaded from the network. isValidItem() if present is an additonal check and does not override the existing checks for expiration and uniqueness.

This is targetted at advanced usage and is strictly optional. The use of unique and expire parameters are the most appropriate way to handle common scenarios.

basket.isValidItem = function (source, obj) {
    return myVerificationFunction(source, obj);
};

The Future

We are currently investigating a number of different features we would like to bring to the project, as well as looking to produce some high-quality performance benchmarks (compared to IndexedDB, Browser cache and more). To find out more, check out what we're working on.

Team, License & Contribution Guide

basket.js is released under an MIT License and is currently maintained by Addy Osmani, Sindre Sorhus, Mat Scales and Andrée Hansson. We would also like to extend our thanks to Rick Waldron for the optimizations he suggested for improving the project.

For more information on our style-guide and how to get involved with basket.js, please see the README in our project repo.

Fork me on GitHub