localStorage: Use it, Don't Abuse It

localstorage-use-not-abuse.appspot.com

Today I'm going to talk about client-side storage - the ability for a website to store data persistently for users, entirely in the browser, using JavaScript.

localStorage: API


method/attribute args returns
setItem String key, String value
getItem String key String value
removeItem String key
clear
key int index String key
length int length

Its a very simple API - basically an associative array with wrappers to get and retrieve the keys, a way to iterate through all the keys stored, and exceptions when something goes wrong like going over quota.

Using localStorage

With the API:

localStorage.setItem('decade', '80s');
localStorage.setItem('song', JSON.stringify({artist: 'Roxette', title: 'Listen to your heart'}));

var decade = localStorage.getItem('decade');
var song = JSON.parse(localStorage.getItem('song'));

alert('Best song in the ' + decade + ' was ' + song.title + ' by ' + song.artist);

With store.js

store.set('decade', '80s')
store.set('song', {artist: 'Roxette', title: 'Listen to your heart'});

var decade = store.get('decade');
var song = store.get('song');

alert('Best song in the ' + decade + ' was ' + song.title + ' by ' + song.artist);

Other libraries: LawnChair, locache

show in CDT store.js is your standard localStorage library, and it includes fallbacks to globalStorage for older FireFox versions and userData for older IE versions - two browser-specific solutions that never made it into standards. Includes serialization and fallbacks(globalStorage/userData).

localStorage...

USE IT

and make your user experience

BETTER

BETTER: Persistent user data

One way we can use it is to remember user data. Sure, you can remember user data on the server and often should, but there are a few times when you might want to save it on the client instead - like if you want to let users have a customized experience on your site without having to sign up, or if you're doing a purely client app like a browser extension or mobile app.

Persistent user data: Europopped

Saves favorites.

europopped-84858380 -1
europopped-99070608 0
europopped-100548743 1

One of my favorite blobs is Europopped, and it catalogs the amazing music and music videos that come out of Europe. I actually really love Europop (in case you didn't catch on yet), so I wanted a way to easily listen to the songs on the blog. I also love maps, so I mashed up the blog posts with a Google map to view them by country. Then when I view a video and like it, I can click the little like button and remember. It's all stored client-side in localStorage. This mashup is completely client-side, and only a few lines of code, and it's just nice that I can add in favorites without needing a server and database. Maybe if anyone else liked europop as much as me, I'd have to add a server.

Persistent user data: dillinger.io

Saves user profile.

profile {"theme":"ace/theme/textmate","showPaper":false, "currentMd":"lscache\nThis is a simple library that emulates `memcache` functions using HTML5 `localStorage`,…

dillinger.io is a Markdown editor that uses localStorage to remember the preferences and last typed text, and let you interact without ever signing in. They do have a sign-in with github option, and ideally they'd migrate from localStorage to the server-side once you signed in with that, so that your preferences would follow you.

Persistent user data: Quiz Cards

Saves what cards the user has seen and how well they've done.

german-answer-mode multiple-choice
german-silverware-Besteck {"lastAsked":1322691448489, "timesAsked":1, "timesCorrect":1, "timesIncorrect":0}
german-ham-Schinken {"lastAsked":1322691462282, "timesAsked":1, "timesCorrect":1, "timesIncorrect":0}
german-bucket1 [{"id":"arm-Arm","lastAsked":0}, {"id":"cheek-Backe","lastAsked":0}, {"id":"belly-Bauch","lastAsked":0},…

I really like learning languages so I made this Chrome extension for doing interactive flash cards. When you install the extension, you get a little icon in your toolbar that you can click to practice interactive flash cards. It uses the Leitner system of learning, so you see cards less the better you do at them - and that's the data I store in localStorage.

BETTER: Persistent app state

Even if you do have a server to store user preferences and data, there are still some pieces of data that can be better served by client-side storage, like the current state of the application. A user doesn't necessarily expect application state to follow them across clients, and it's sometimes overkill to store that in the server. (And you can always migrate it to the server later if you want).

Persistent app state: jshint.com

Remembers checked options.

opts {"debug":true, "forin":true, "eqnull":false, "noarg":true, "noempty":false, "eqeqeq":true, "boss":false, "loopfunc":true, "evil":true, "laxbreak":true, "bitwise":true, "strict":true, "undef":true, "curly":true, "nonew":true, "browser":true, "devel":false, "jquery":false, "es5":false, "node":false}

JSHint is a tool which checks your JS code quality and includes many options, since JavaScript style varies. On the online version, you can check the options and it remembers that for the next time you visit.

BETTER: Filled-in forms

Specialized libraries: autoStorage, Savify

You can also remember user entered form input - like their login username, information in forms that they fill out often, and long text input that they perhaps meant to save but didn't for whatever reason. It's a great use of localStorage because the form input doesn't *need* to be remembered, but it can bring a lot of joy to the user if it is - like magic. You just have to consider carefully which parts of a form should actually be remembered, and for how long.

Filled-in forms: jsperf.com

Remembers the author information.

author-email pamela.fox@gmail.com
author Pamela Fox
author-url http://pamelafox.org

On jsperf.com, they have a form for creating new test cases. It remembers your author information, since that's always the same, and it also uses the same author information to auto-populate the comments creation form.

Filled-in forms: Disqus

Remembers unposted comment drafts.

disqus.drafts [{"thread:delayed_image_loading_on_long_pages": "I'm going to write a really long and epic comment, and I will be really mad if I accidentally move away from the page and lose this comment, mmm kay?"}]

Disqus is a popular embedded commenting system, and they added a feature to remember the text that you typed in a comment box but didn't post, so that if you accidentally leave a page and come back, your comment draft will still be there. The new Twitter does this, and they also save an expiration so they don't show the draft after a certain point.

BETTER: A faster site

Specialized libraries: lscache, lscache jQuery plugin YQL LocalCache, Inject, Basket.js

Websites are increasingly reliant on AJAX requests and API requests, and many times these requests can be cached so users dont have to wait so long. Yes, you can set cache headers on the server so that the browser serves them out of its cache, but there are sometimes when it's better to use localStorage. You have more control when you do the caching yourself in the client- you can invalidate the resources yourself, and you can cache requests for different amounts of time in different areas of your site. Plus, mobile browsers have smaller caches and can do less HTTP requests, so you can improve your performance significantly there by using localStorage for caching resources. You can also improve the *apparent* loading speed for a page, by loading in old data first from localStorage, then refreshing with new data.

A faster site: RageTube

Caches parsed playlists, Youtube API search results.

youtube: Hot Chip I Feel Better[{"id":"5GOZjlwIwfk","uploaded":"2010-03-17T17:53:17.000Z","category":"Music","title":"Hot Chip - I Feel Better",…
youtube: Hot Chip I Feel Better-expiration 22153109
parser: http://www.a… {"songs":[{"artist":"Angus & Julia Stone","title":"Big Jet Plane","id":"angusandamp; julia stone-big jet plane"},…

Here's another music video mashup of mine, which I made when I was in Australia and couldn't use Pandora or Spotify or any good streaming music service. RageTube turns online playlists from an Australian TV channel into Youtube videos, and it uses two APIs - one to parse the playlists, and the other to parse the Youtube API search results. I use lscache to parse both these API results - the playlist indefinitely, and the API search results for a few days, because video searches don't change that often. So if a user often loads in the same playlist, RageTube will make much less requests to the APIs.

A faster site: Mobile Facebook

Caches autocomplete data.

typeahead {"time": 1329151694363, "value": {"friends": [{"path": "/anton.kovalyov", "photo": "http://profile.ak.fbcdn.net/hprofile-ak-snc4/186412_506803098_992151709_q.jpg", "text": "Anton Kovalyov", "uid": 506803098,},…

Facebook mobile uses localStorage to make friend searches and autocompletes faster, by fetching your friends and caching. It's a great example of where it's okay if the data is slightly stale. Twitter does something similar.

A faster site: Google Mobile

Caches JS and CSS files.

mres.-8Y5Dw_nSfQztyYx <style>a{color: #11c} a:visited{color: #551a8b} body{margin:0;pad…
mres.-Kx7q38gfNkQMtpx <script> //<![CDATA[ var Zn={},bo=function(a,b){b&&Zn[b]||(ne…
mres:time.-8Y5Dw_nSfQztyYx 1301368541872
mres:time.-Kx7q38gfNkQMtpx 1301368542755
  var c=localStorage.getItem("mres."+a);
  if (c) {
    document.write(c);
    localStorage.setItem("mres:time."+a,Date.now());
  } else {
     window._clearCookie("MRES");
     document.location.reload(!0);
  }

Both Google Mobile and Bing mobile search use localStorage to cache their HTML, CSS, and JS, to ultimately make their search page download smaller. It's a little complicated, but basically they assign IDs to parts of their webpage, store them in localStorage, remember the IDS and expirations in a cookie, and when the server sees that cookie, it doesn't re-send those stored parts.

A faster site: ESRI Maps

Caches map tile images.

http://server…/tile/12/1409/2075 /9j/4AAQSkZJRgABAQEAYABgAAD/2wB…
http://server…/tile/12/1410/2077 /9j/4AAQSkZJRgABAQEAYABgAAD/2wB…

You can actually use localStorage to cache images, by converting them from binary data into data URIs. If you use a massive number of images on your site, you might want to do this. Here's an example from the ESRI maps API of caching the map tile images - each key is the tile URL, and the value is the data URI.

BETTER: An offline site

Specialized libraries: jQuery offline

Do everything that you did to increase performance, but do it so that the app can go offline. But think about the user experience. It might make sense to have stale data for a few minutes on the web, but what about for a few hours on mobile? Or a whole day? And here you have to cache all the necessary data for the app to work, not just the data that will make the app more performant.

An offline site: EatDifferent

Caches user profile and log data.

lscache-user {"first_name":"Testy", "last_name":"McTesterFace", "id", 166, …
lscache-166:log02/14/2012 {"measurements":{"weight":{"units":"","value":"150"}, "meals":[{"what":"sausage","when":"12:00pm",…

For the mobile app for EatDifferent, I use localStorage so that it works when the user is offline. It remembers the information about the last logged in user, as well as previously loaded daily logs. Once it gets a connection to the network, it tries to re-fetch both the user and log data, and then refreshes the UI with the new data.

ABUSE IT

and make your user experience

WORSE

WORSE: A slower site.

So:

Just like localStorage can make your site faster, misusing it can make it slower. This is the main way that using localStorage can make your site worse.

How do you know what's slowing down your site?

Time your site in target browsers and find the slow points.

Blog post: Measuring performance in PhoneGap, Blog post: Performance Profiling in JavaScript, JS Performance Analysis Tools

Don't: serialize unnecessarily

Do: use strings where possible

Before:

function store(key, val) {
  localStorage.setItem(key, JSON.stringify(val));
}
store('num', 1);
store('on', true);
store('name', 'pamela');

After:

function store(key, val) {
  localStorage.setItem(key, val);
}
store('num', '1');
store('on', 'true');
store('name', 'pamela');

How much faster?

jsperf: Primitives vs. Strings jsperf: Optional use of JSON stringify

Don't: serialize unnecessarily

Do: use your own serializer if its faster

Before:

var fruits = ['apple', 'banana', 'orange'];
localStorage.setItem('fruits', fruits.join(','));
var fruits = localStorage.getItem('fruits').split(',');

After:

var fruits = ['apple', 'banana', 'orange'];
localStorage.setItem('fruits', JSON.stringify(fruits));
var fruits = JSON.parse(localStorage.getItem('fruits'));

How much faster?

jsperf: String split/join vs. JSON stringify

Don't: serialize unnecessarily

Do: store multiple strings instead of 1 object if its faster

Before:

localStorage.setItem('location', JSON.stringify({'state': 'texas', 'city': 'austin'}));

After:

localStorage.setItem('location-state', 'texas');
localStorage.setItem('location-city', 'austin');

How much faster?

jsperf: 2 keys vs. JSON, jsperf: Multiple keys vs. JSON

Don't: use excessive keys

Do: combine keys commonly accessed together

Before:

localStorage.setItem('first', 'pamela');
localStorage.setItem('middle', 'susan');
localStorage.setItem('last', 'fox');

After:

localStorage.setItem('name', 'pamela susan fox');

How much faster?

jsperf: 1 long key vs. multiple short keys

Balance need for serialization vs time it takes to access.

Don't: do excessive gets/sets

Do: cache data in local memory or the DOM, and only get/set on window load/unload.

Before:

$('input[type="checkbox"]').click(function() {
  localStorage.setItem($(this).attr('name'), $(this).is(':checked'));
});

After:

window.onunload = function() {
  $('input[type="checkbox"]').each(function() {
    localStorage.setItem($(this).attr('name'), $(this).is(':checked'));
  });
};

Examples:

Exercise Explorer: Caching in local memory, Hearty Extension: Caching in the DOM

Don't: block the UI

Do: defer using localStorage until onload

Before:

<head>
<script>
  $('#name').html(localStorage.getItem('name'));
</script>
</head>

After:

<html>
<body></body>
<script>
window.onload = function() {
  $('#name').html(localStorage.getItem('name'));
};
</script>
</html>

Not Blocking the UI in Tight JS Loops,

Many libraries actually do a localStorage get/set when loaded, watch out for that- like Modernizr. Defer!

Don't: block the UI

Do: use setTimeout to defer localStorage access

Before:

$('button').click(function() {
  var name = localStorage.getItem('name');
  $('#name').html(name);
});

After:

$('button').click(function() {
  window.setTimeout(function() {
    var name = localStorage.getItem('name');
    $('#name').html(name);
  }, 10);
});

Nicholas Zakas: Responsive Interfaces,

Don't: block the UI

Do: throttle or debounce to avoid repetitive gets/sets

Before:

$('textarea').keydown(function() {
  localStorage.setItem('text', $(this).text());
});

After:

$('textarea').keydown(function() {
  $.debounce(250, function() {
    localStorage.setItem('text', $(this).text());
  });
});

Blog Post: 2 LocalStorage Tips jQuery Throttle/Debounce Plugin

WORSE: A dysfunctional site.

Don't: assume localStorage works or will always work.

Do: check for feature support, check if its read/write, and check if its over quota.

Bad:
localStorage.setItem('bla', 'bla');
Better:
if (window.localStorage) {
  localStorage.setItem('bla', 'bla');
}
Best:
if (window.localStorage) {
  try {
    localStorage.setItem('bla', 'bla');
  } catch(e) {
    if (e.name === 'QUOTA_EXCEEDED_ERR' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED') {
    } else {
    }
  }
}

Most localStorage libraries take care of this for you.

// incognito mode

Don't: use key names that collide.

Do: use highly descriptive keys with pseudo namespaces.

Before:
localStorage.setItem('name', 'pamela');
After:
localStorage.setItem('conference-speaker-first-name', 'pamela');
After:
lscache.setBucket('conference-speaker');
lscache.set('first-name', 'pamela');

No excuses: jsperf: Long vs. short names

Know your options

Size Data Perf
cookies 20 * 4KB Strings Slow load
localStorage 2-5MB Strings Slow access
IndexedDB ∞? Objects Async access
File API ∞? Text,Binary Async access

Understanding Client-side Storage in WebApps

...And your browsers

IE FF Chrome Safari Opera iOS Android
cookies
localStorage 8.0+ 3.5+4.0+ 4.0+ 10.5+ 3.2+ 2.1+
IndexedDB 10 FF4+11.0+
File API 13.0

caniuse.com

Use:

Don't abuse:



friends dont let friends abuse localStorage