localstorage-use-not-abuse.appspot.com
Pamela Fox - @pamelafox
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.
| 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 |
Read more: localStorage spec
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.
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).
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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);
}
Read more: Bing & Google Case Study
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.
Caches map tile images.
| http://server…/tile/12/1409/2075 | /9j/4AAQSkZJRgABAQEAYABgAAD/2wB… |
| http://server…/tile/12/1410/2077 | /9j/4AAQSkZJRgABAQEAYABgAAD/2wB… |
Read more: Storing tiles in localStorage
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.
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.
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.
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.
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
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
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
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
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.
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
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!
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);
});
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
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
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
| 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
| 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 |
friends dont let friends abuse localStorage