Chapter 6. Optimizing with Web Storage

Today, you have two widespread and well-supported mechanisms for storing data on the client: cookies and Web Storage. Many say that Web Storage is the evolution of cookies, but in reality, cookies may stick around a lot longer than people think. Mainly because they are much different than Web Storage and implicitly send data back to the server upon each request through HTTP headers. Cookies are always available on the server and can be read and written to freely, which is great for user session management and similar situations. The downside is that you only get 4kb of storage per cookie.

Web Storage is different from cookies in that the stored data is not shared with the server. You can currently store 5MB of data on the client device with Web Storage, and some browsers allow up to 10MB of storage with user approval. However, these limits can be a little misleading. If you try to store 5MB of pure HTML in local storage within WebKit-based browsers, such as Mobile Safari, you will see that it allows for a maximum of 2.6MB only. For this, you can thank section 7.4 of the first W3C Working Draft of the Web Storage specification, which states:

In general, it is recommended that userAgents not support features that control how databases are stored on disk. For example, there is little reason to allow Web authors to control the character encoding used in the disk representation of the data, as all data in JavaScript is implicitly UTF-16.

Although section 7.4 was removed in following drafts, most WebKit browser vendors have stuck to the UTF-16 implementation of data encoding. Two exceptions, Firefox and IE, give you the actual 5MB of storage space.

Web Storage offers two storage objects—localStorage and sessionStorage—both of which are widely supported from IE8 upward and in all modern browsers, including mobile devices. (For browsers that don’t support Web Storage natively, it includes several polyfills.) Both storage objects use exactly the same APIs, which means that anything you can do with localStorage, you can also do with sessionStorage and vice versa. With sessionStorage, however, your data is stored only while that particular browser window (or tab) is open. After the user closes the window, the data is purged. With localStorage, your data will stay on the client across browser sessions, device restart, and more. Any data stored is tied to the document origin, in that it’s tied to the specific protocol like HTTP or HTTPS, the top-level domain (for example html5e.org), and the port, usually port 80. One more caveat regarding sessionStorage: if the browser supports resuming sessions after restart, then your sessionStorage object may be persisted unknowingly. This can be an issue if your use case expects the sessionStorage data to be destroyed upon closing of the browser.

Web Storage defines two APIs, Storage and StorageEvent, which either local or session storage can use. This means you have two ways of working with and managing local data. Whichever API you choose, remember with Web Storage, operations are synchronous: When you store or retrieve data, you are blocking the main UI thread, and the rest of the page won’t render until your data operations are finished.

As this usage example illustrates, the Storage API offers multiple ways of working with your data in a storage object:

localStorage.bookName = 'HTML5 Architecture';
//or
localStorage['bookName'] = 'HTML5 Architecture';
//or
localStorage.setItem('bookName') = 'HTML5 Architecture';

You can treat your data like any other JavaScript object and add the key directly as a localStorage property, which calls setItem() behind the scenes. Your available functions are:

.length //returns the number of key/value pairs
.key(n) //returns the name of the nth key in the list
.getItem(key) //returns the current value associated with the key
.setItem(key, value) //creates new or adds to existing key
.removeItem(key) //you can probably guess what this does :)
.clear() //removes everything from storage associated with this domain

You might think that all of the methods for storing data have the same performance, but that would be crazy talk in web browser land, right? Figure 6-1 and Figure 6-2 provide a more realistic view and a performance analysis (http://jsperf.com/localstorage-getitem-setitem-vs-getter-setter/4) on which storage approach works the best.

Instead of using localStorage.setItem() and calling the object setter property directly, with localStorage.small or localStorage['small'], you can give your data storage a 50% speed boost in Chrome (Figure 6-1). The same performance test in the latest Firefox and Safari web browsers, however, reveals that localStorage.setItem() performs better than the others for small values (Figure 6-2).

As for most applications, you want your web app to perform at top speed across all browsers. Usually, a real-world application will store a larger JSON object, base64 image string, or HTML snippet in localStorage. As you can see with the largeValue tests in the figures, all Storage API options perform roughly the same.

Although Web Storage is considered to be “racy” (more on this in a moment), you can avoid most race conditions by using the StorageEvent API. If the user has the same site open in different tabs, this event can be used to synchronize the data. Here’s a sample usage:

window.addEventListener('storage', function(evt){alert(evt.key)}, false);
.key //returns the value of the key being changed. It's null upon creation.
.oldValue //represents the old value of the key being changed
.newValue //represents the new value of the key being changed
.url //the address of the document whose key changed
.storageArea //the Storage object which was affected. Either localStorage or sessionStorage

The storage event is fired only when the new value is not the same as the old value. The storage event contains the key, oldValue, and newValue properties of data that has changed, which you can access in code. This example creates the appropriate event listener, which logs the oldValue and newValue across all open browser sessions:

window.addEventListener('storage', function(event) {
  console.log('The value for ' + event.key + ' was changed from' + event.oldValue 
                                                       + ' to ' + event.newValue);
  }, false);

Note

The storage event fires on the other windows only. It won’t fire on the window that did the storing.

To store a JavaScript object (or an array perhaps) in your localStorage or sessionStorage, you need to use JSON to encode and decode your data, as in:

var mydata = {
    "Book" : "HTML5 Architecture",
    "Author" : "Wesley Hales",
};

Next, store the JavaScript object as a string:

localStorage.setItem("mydata", JSON.stringify(mydata));

When you’re ready to retrieve the data, use:

JSON.parse(localStorage.getItem("mydata"));

All this communication between client and server raises security issues. They come in two flavors: keeping your app secure and private browsing by users.

Because of the potential for DNS spoofing attacks, you cannot guarantee that a host claiming to be in a certain domain really is from that domain. To mitigate this and keep your app secure, you can use TLS (Transport Layer Security) for your pages. TLS and its predecessor, Secure Sockets Layer (SSL), are cryptographic protocols that provide communication security over the Internet. Pages using TLS can be sure that only the user, software working on behalf of the user, and other pages using TLS that have certificates identifying them as being from the same domain, can access their storage areas.

Web Storage, both localStorage and sessionStorage, is not secure and is stored in plain text with no way to encrypt. If you’re worried about data security, don’t use localStorage. There are solutions like JCryption (http://www.jcryption.org) for those unwilling to buy SSL certificates or with hosting providers who do not support SSL. It’s no replacement for SSL, because there is no authentication, but the jCryption plug-in offers a base level of security while being very easy and quick to install.

Warning

Be aware that any form of JavaScript encryption is intrinsically vulnerable to man-in-the-middle (MITM) attacks, so it is not a recommended practice for storing sensitive data.

Within certain browsers, while the user is running in private or incognito browsing modes, your application will get an exception when trying to store anything in Web Storage. Every app that uses localStorage should check window['localStorage'].setItem for a rising QUOTA_EXCEEDED_ERR exception before using it. For example, the problem in Figure 6-3 is that the window object still exposes localStorage in the global namespace, but when you call setItem, this exception is thrown. Any calls to .removeItem are ignored.

Safari returns null for any item that is set within the localStorage or sessionStorage objects. So even if you set something before the user goes into private browsing mode, you won’t be able to retrieve until they come out of the private session.

Chrome and Opera will allow you to retrieve items set before going into incognito mode, but once private browsing commences, localStorage is treated like sessionStorage (only items set on the localStorage by that session will be returned).

Firefox, like Chrome, will not retrieve items set on localStorage prior to a private session starting, but in private browsing treats localStorage like sessionStorage.

To be safe, always do a series of checks before using localStorage or sessionStorage:

function isLocalStorageSupported() {
    try {
        var supported = ('localStorage' in window &&
                         window['localStorage'] !== null);
        if (supported) {
          localStorage.setItem("storage", "");
          localStorage.removeItem("storage");
        }
        return supported;
    } catch(err) { return false; }
}

Take a look at Table 6-1, an overview of the five most visited sites on the Internet, to get an idea of which sites are (or aren’t) using Web Storage to optimize.

For mobile, Google’s basic search page is making the most use of localStorage by storing base64 images and other CSS. For each subsequent page request, it uses JavaScript to insert <style> blocks just after the page title in the document head with the CSS values from localStorage (Figure 6-4).

For a basic Google search on the desktop, data is stored differently than on mobile. First, sessionStorage is used, so you know this will be temporary data. Looking at the raw JSON data stored by a simple Google search in Figure 6-5, you can see mostly CSS and HTML is stored along with some namespaced tracking data.

Twitter also makes heavy use of localStorage on mobile devices. Looking at the JSON saved on the device in Figure 6-6, you can see that Twitter stores all of the data required to present the user interface. The data isn’t a straight dump of HTML to localStorage, however, it’s organized in a JSON object structure with proper escaping and JavaScript templating variables.

Amazon’s use of sessionStorage is minimal tracking information related to “product likes.” But overall, it’s a bit surprising to see that the top sites on the Internet are still not leveraging Web Storage to speed up their site and reduce HTTP requests.

Efficient requests and zippier interfaces may not be a huge problem for desktop sites, but there’s no reason we shouldn’t have these storage enhancements on both mobile and desktop. Some of the reasons we’re seeing heavy Web Storage usage only on mobile are:

Note

When using base64-encoded data URIs, be aware that the encoded data is one third larger in size than its binary equivalent. (However, this overhead is reduced to 2 to 3% if the HTTP server compresses the response using GZIP.)

As you have seen, there are a million different ways to use Web Storage within your application. It really comes down to answering a few questions:

Of course, after seeing that Web Storage blocks the main JavaScript UI thread when accessing data, you must be considerate of how your page loads and use best practices for storing and retrieving data.

The best place to start with localStorage is using it where your app requires user input. For example, if you have a comments input box within a form, you could use localStorage to save a draft on the user input in case the session times out or the form is submitted improperly. The commenting service Disqus follows this practice and saves your draft comments in localStorage.

Most web services allow you to hit their service a limited number of times per day. By using localStorage with a timestamp, you can cache results of web services locally and access them only after a specified time to refresh the data.

A simple library that allows for this memcache-like behavior is lscache (https://github.com/pamelafox/lscache). lscache emulates memcache functions using HTML5 localStorage, so that you can cache data on the client and associate an expiration time with each piece of data. If the localStorage limit (about 5MB) is exceeded, it tries to create space by removing the items that are closest to expiring anyway. If localStorage is not available at all in the browser, the library degrades by simply not caching, and all cache requests return null.

All of the ways for Web Storage to speed up your web application discussed so far, are forms of one-way communication for which syncing, or transmitting modified JSON objects, back to the server is not required. Instead, you simply push data to the browser and use it as a cache.

Today, companies are just starting to leverage Web Storage to store and sync the object model back to the server-side database. Such functionality is useful to:

Some of these data management and versioning situations can get fairly complex. For example, LinkedIn recently posted its solution to managing RESTful JSON data with localStorage. The company’s main reasoning for bringing localStorage into the picture was to reduce latency and unneeded network requests on its latest iPad app. According to LinkedIn engineer Akhilesh Gupta:

For the full article, see http://engineering.linkedin.com/mobile/linkedin-ipad-using-local-storage-snappy-mobile-apps.

At its core, this particular application uses Backbone to manage client-side data models. The developers then wrote the necessary code to override the basic sync functionality to allow models and collections to be stored in localStorage. Again, this is clearly a performance move and doesn’t really address syncing data back to the server. But, it is a more complex use case that manages versioning and migration of the data to newer versions of the app. In the end, the iPad application gained the following performance improvements:

  • A more responsive application thanks to temporarily storing recently fetched data; users no longer have to wait for network requests to finish before moving around the application

  • Seamless sharing of fetched data among multiple web views in the native application

  • Independence from memory constraints in mobile devices; localStorage can store and populate temporary objects in memory when necessary

  • Decreased memory footprint and rendering time while scrolling because complicated HTML document fragments are stored in localStorage

A few frameworks allow for data to be synced from localStorage back to the server. For example, Backbone.js comes with methods for fetching and saving data models to and from the server. Out of the box, however, it does not provide the advanced functionality required by an application that needs to work offline and synchronize with the server when online. To address this, Neil Bevis of the Dev Camp blog posted an excellent solution that I’ll summarize here. (For the complete blog post, see http://occdevcamp.wordpress.com/2011/10/15/backbone-local-storage-and-server-synchronization.)

Backbone-localstorage.js provides communication with localStorage by simply adding the JavaScript file to the project. By adding this file, however, you then cannot communicate between Backbone and the server with Backbone.sync. The first thing you must do is create a copy of the Backbone.sync method before it’s replaced by the inclusion of the backbone-localstorage.js JavaScript file:

<script src="backbone.js"></script>
<script>Backbone.serverSync = Backbone.sync;</script>
<script src="backbone-localstorage.js"></script>

Now, you’ll be able to save data to the server using:

Backbone.serverSync('update', model, options);

This gives the standard model.fetch() and model.save() functions the ability to use localStorage. Next, you must provide a synchronized flag with a Boolean value describing its client-side status. When the client is ready to push local changes to the server from a given collection, it sends model objects with synchronized=false on a model-by-model basis using:

 Backbone.serverSync('update', model, { success: 'foo', error: 'bar'}).

If the server responds with a different ID than what is stored on the client, then that means you have a new object. If the IDs remain the same, however, then you simply have an update. When a new object comes from the server, the following code deletes the existing ID in localStorage and adds the new version:

for (var i = 0; i < models.length; i++) {
        var model = models[i];
        if (model.get('synchronized')) { continue; }
        model.change();
        Backbone.serverSync('update', model, {
            success: function (data) {
                var model = collection.get(data.ClientId);
                //if new server will return a different Id
                if (data.ServerId != data.ClientId) {
                    //delete from localStorage with current Id
                    Backbone.sync("delete", model,
                     { success: function () { },
                       error: function () { } });

                    //save model back into localStorage
                    model.save({ Id: data.ServerId })
                }
                model.save({ synchronized: true });
                collection.localCacheActive = false;
            },
            error: function (jqTHX, textStatus, errorThrown) {
                console.log('Model upload failure:' + textStatus);
                collection.localCacheActive = false;
            }
        });
    }

When asked to pull server-side changes to a collection from the server, the client first uses model.save() to save any unpushed client-side changes into localStorage. It next requests the entire collection from the server via the standard Backbone fetch method:

tempCollection.sync = Backbone.serverSync;
tempCollection.fetch( { success: blah, error: blah });

In practice, you could reduce the associated data download to only items that require updating. As it receives each model back from the server, the success function checks each one against its own list. If the model is new, success adds it to the collection that is updating and also uses model.save() to record it into local storage:

collection.add(tempModel);
tempModel.change();
tempModel.save({ synchronized: true });

Finally, the success function updates the model with revised data after the model has been synchronized:

model.set(tempModel.toJSON());
model.set({ synchronized: true });
model.save();

The big issue with this approach is if the model already exists and the user has made localStorage-based modifications to it. In this code, those models are not updated during the pull of server-side changes. Those objects are pushed to the server to be updated in the database.

This is not an end-all solution, and there are many frameworks currently trying to address this problem. Many of the solutions are just as mature as the one reviewed here. So your use of localStorage and syncing to a server-side database will be dictated by the complexity of your use case.

Although you can use localStorage safely within most modern web browsers, if your application must accommodate browsers without localStorage, you can use some easy-to-follow, lightweight polyfills. For example, the following example polyfill accommodates IE 6 and 7, as well as Firefox 2 and 3. With the exact same API as defined in the Web Storage spec, you can start using it today with roughly 90 lines of JavaScript included in your application. (For the full source, see https://raw.github.com/wojodesign/local-storage-js/master/storage.js.)

(function(){
    var window = this;
    // check to see if we have localStorage or not
    if( !window.localStorage ){

        // globalStorage
        // non-standard: Firefox 2+
        // https://developer.mozilla.org/en/dom/storage#globalStorage
        if ( window.globalStorage ) {
            // try/catch for file protocol in Firefox
            try {
                window.localStorage = window.globalStorage;
            } catch( e ) {}
            return;
        }

        // userData
        // non-standard: IE 5+
        // http://msdn.microsoft.com/en-us/library/ms531424(v=vs.85).aspx
        var div = document.createElement( "div" ),
            attrKey = "localStorage";
        div.style.display = "none";
        document.getElementsByTagName( "head" )[ 0 ].appendChild( div );
        if ( div.addBehavior ) {
            div.addBehavior( "#default#userdata" );

            var localStorage = window["localStorage"] = {
                "length":0,
                "setItem":function( key , value ){
                    div.load( attrKey );
                    key = cleanKey(key );

                    if( !div.getAttribute( key ) ){
                        this.length++;
                    }
                    div.setAttribute( key , value );

                    div.save( attrKey );
                },
                "getItem":function( key ){
                    div.load( attrKey );
                    key = cleanKey(key );
                    return div.getAttribute( key );

                },
                "removeItem":function( key ){
                    div.load( attrKey );
                    key = cleanKey(key );
                    div.removeAttribute( key );

                    div.save( attrKey );
                    this.length--;
                    if( this.length < 0){
                        this.length=0;
                    }
                },

                "clear":function(){
                    div.load( attrKey );
                    var i = 0;
                    while ( attr =
                   div.XMLDocument.documentElement.attributes[ i++ ] ) {
                        div.removeAttribute( attr.name );
                    }
                    div.save( attrKey );
                    this.length=0;
                },

                "key":function( key ){
                    div.load( attrKey );
                    return
                   div.XMLDocument.documentElement.attributes[ key ];
                }

            },

            // convert invalid characters to dashes
            // http://www.w3.org/TR/REC-xml/#NT-Name
            // simplified to assume the starting character is valid
            cleanKey = function( key ){
            return key.replace( /[^-._0-9A-Za-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u37f-\
                                  u1fff\u200c-\u200d\u203f\u2040\u2070-\u218f]/g, "-" );
            };


            div.load( attrKey );
            localStorage["length"] =
            div.XMLDocument.documentElement.attributes.length;
        }
    }
})();

A few JavaScript frameworks address Web Storage needs on mobile devices. When evaluating Web Storage frameworks, look for a nice consistent storage API that works across all devices. Of course, this is what the spec itself does through a simple JavaScript API, but until all devices support this specification, you need a helper framework.

LawnChair (http://westcoastlogic.com/lawnchair) is designed with mobile in mind. Supporting all major mobile browsers, it’s adaptive to the mobile and desktop environments described in this book and gives you a consistent API for accessing some form of localStorage. LawnChair allows you to store and query data on browsers without worrying about the underlying API. It’s also agnostic to any server-side implementations, enabling you to get started quickly with a simple, lightweight framework.

The page setup is:

<!DOCTYPE html>
<html>
<head>
    <title>my app</theitle>
</head>
<body>
    <script src="lawnchair.js"></script>
</body>
</html>

To persist data, use:

Lawnchair(function(){
    this.save({msg:'hooray!'})
})

Supporting all major mobile browser platforms, persistence.js (http://persistencejs.org) is an asynchronous JavaScript object-relational mapper. It integrates with Node.js and server-side MySQL databases and is recommended for server-side use, because using in-memory data storage seems to slow down filtering and sorting. The download size is much heavier than that of LawnChair.

For page setup, use:

<!DOCTYPE html>
<html>
<head>
    <title>my app</title>
</head>
<body>
    <script src="persistence.js" type="application/javascript"></script>
<script src="persistence.store.sql.js" type="application/javascript"></script>
<script src="persistence.store.websql.js" type="application/javascript"></script>
</body>
</html>
if (window.openDatabase) {
    persistence.store.websql.config(persistence, "jquerymobile", 'database',
                                                            5 * 1024 * 1024);
} else {
    persistence.store.memory.config(persistence);
}

  persistence.define('Order', {
    shipping: "TEXT"
  });

  persistence.schemaSync();

Similar to Hibernate (JBoss’s persistence framework), persistence.js uses a tracking mechanism to determine which objects changes have to be persisted to the database. All objects retrieved from the database are automatically tracked for changes. New entities can be tracked and persisted using the persistence.add function:

var c = new Category({name: "Main category"});
persistence.add(c);

All changes made to tracked objects can be flushed to the database by using persistence.flush, which takes a transaction object and callback function as arguments. You can start a new transaction using persistence.transaction:

persistence.transaction(function(tx) {
  persistence.flush(tx, function() {
    alert('Done flushing!');
  });
});