JavaScript data storage — localStorage, IndexedDB and CacheStorage

This article gives all you want to know about different data storage including localStorage, sessionStorage, IndexedDB and Cache. After reading this post, you will get to know the differences among them, which storage should you use, how to use them, how much data can I store, etc.

Overview

An overview of the storage types.

  • Web Storage
    • sessionStorage Temporary session data

    • Store key/value pair only for a session, data expires when browser(or tab) is closed.

    • The keys and values are always in the UTF-16 string format.

    • localStorage Small data

    • Store key/value pair persistently.

    • The keys and values are always in the UTF-16 string format.

    • Useful for storing small amounts of data.

  • [IndexedDB](IndexedDB API – Web APIs | MDN) Large data

    • A transactional database system,

    • Powerful but complicated for simple cases.

    • Useful for storing large amounts of structured data.

  • Cache Resource caching

    • Store Request / Response object pairs, representing HTTP requests and responses.
    • Useful in URL addressable resource caching, like assets, HTTP API responses.

Note

Cookies should not be used for storage. They are sent with every HTTP request. If storing data with it will significantly increase the size of a HTTP request.

Deprecated API for storage

Differences among data storage types

Here are differences among the 4 types of data storage.

Storage Expire Asynchronous Size limit Security Web workers
sessionStorage ✓ (Yes) ✗ (no) 5MB Same tab ✗ (No)
localStorage ✗ (No)* ✗ (No) Same origin ✗ (No)
IndexedDB ✗ (No)* ✓ (Yes) Large Same origin ✓ (Yes)
Cache ✗ (No)* ✓ (Yes) ✓ (Yes)

More about expire

If storage limit is reached, data may be cleared out depending on storage type and the browser.

For Safari, data will be deleted after 7 days of use.

webkit: 7-Day Cap on All Script-Writeable Storage

In iOS and iPadOS 13.4 and Safari 13.1 on macOS, it will delete all of a website’s script-writable storage after seven days of Safari use without user interaction on the site. These are the script-writable storage forms affected (excluding some legacy website data types):

  • Indexed DB
  • LocalStorage
  • Media keys
  • SessionStorage
  • Service Worker registrations and cache

More about security

Access to Web Storage from third-party IFrames is denied if the user has disabled third-party cookies (Firefox implements this behavior from version 43 onwards.)

Note

Cookies use a separate definition of origins compared to that used by Web Storage and IndexedDB. A page can set a cookie for its own domain or any parent domain, as long as the parent domain is not a public suffix.

Storage limit

The global limit for all kinds of storage is dynamic and depends on the browser. According to Storage for the web:

  • Firefox:  50% of free disk space.

  • Chrome: 80% of total disk space. An origin can use up to 60%

  • Safari (both desktop and mobile): appears to allow about 1GB.

What happen if the storage limit is reached?

Depending on the browser, data eviction may be triggered or a QuotaExceededError may be thrown out. The data eviction is based on an LRU policy — the least recently used origin will be deleted first, then the next one, until the browser is no longer over the limit.

See MDN: Browser storage limits and eviction criteria for more information.

Persistent storage

Storage for the web:

You can request persistent storage for your site to protect critical user or application data. Unless the user removes it, you can protect it from eviction.

// Check if site's storage has been marked as persistent
if (navigator.storage && navigator.storage.persist) {
    const isPersisted = await navigator.storage.persisted();
    console.log(`Persisted storage granted: ${isPersisted}`);
} 

// Request persistent storage for site
if (navigator.storage && navigator.storage.persist) {
    const isPersisted = await navigator.storage.persist();
    console.log(`Persisted storage granted: ${isPersisted}`);
}

How much storage space is taken and available?

In many browsers, you can use [StorageManager API](StorageManager.estimate() – Web APIs | MDN) to asks the Storage Manager for how much storage the current origin takes up (usage), and how much space is available (quota).

This method operates asynchronously, so it returns a Promise which resolves once the information is available. The promise’s fulfillment handler is called with an object containing the usage and quota data.

Example

 if (navigator.storage && navigator.storage.estimate) {
    navigator.storage.estimate().then(function(estimate) {
        console.log('You are currently using about '
            + (estimate.usage / estimate.quota * 100).toFixed(2)
            + '% of your available storage.');
        console.log('You can write ' + (estimate.quota - estimate.usage)
            + ' more bytes.');
    });
}

Storage inspect in DevTools

In DevTools, you can inspect the different storage types and clear data. Chrome v88 provides a feature for you to simulate storage quota. See Storage inspect (The content is also quoted in the end of the article) for more details.

sessionStorage

Opening multiple tabs/windows with the same URL creates sessionStorage for each tab/window. It means sessionStorage does not share between multiple tabs/windows.

Availability detecting

See the same section of localStorage.

Basic usage example

// Save data to sessionStorage
sessionStorage.setItem('key', 'value');

// Get saved data from sessionStorage
let data = sessionStorage.getItem('key');

// Remove saved data from sessionStorage
sessionStorage.removeItem('key');

// Remove all saved data from sessionStorage
sessionStorage.clear();

To write/read some data rather than string, use JSON.stringify() to convert it to string and use JSON.parse() to convert it from string.

localStorage

localStorage is similar to sessionStorage, except that while localStorage data has no expiration time.

Availability detecting

Before using localStorage, it is necessary to detect whether it is available, because it may throw exceptions like Failed to read the loacalStorag in various situations. Below are some of them:

  • localStorage is not supported in an early version of a browser.

  • The user disables web storage in the browser settings, like the cookie is blocked on Chrome.

  • In a browser’s private browsing mode.

MDN gives a function that detects whether localStorage or sessionStorage is both supported and available:

function storageAvailable(type) {
    var storage;
    try {
        storage = window[type];
        var x = '__storage_test__';
        storage.setItem(x, x);
        storage.removeItem(x);
        return true;
    }
    catch(e) {
        return e instanceof DOMException && (
            // everything except Firefox
            e.code === 22 ||
            // Firefox
            e.code === 1014 ||
            // test name field too, because code might not be present
            // everything except Firefox
            e.name === 'QuotaExceededError' ||
            // Firefox
            e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
            // acknowledge QuotaExceededError only if there's something already stored
            (storage && storage.length !== 0);
    }
}

And here is how you would use it:

if (storageAvailable('localStorage')) {
  // Yippee! We can use localStorage awesomeness
}
else {
  // Too bad, no localStorage for us
}

Call storageAvailable('sessionStorage') for sessionStorage.

Basic usage example

// Add a localStorage item
localStorage.setItem('myCat', 'Tom');

// Read the localStorage item
const cat = localStorage.getItem('myCat');

// Remove the localStorage item.
localStorage.removeItem('myCat');

To write/read some data rather than string, use JSON.stringify() to convert it to string and use JSON.parse() to convert it from string.

IndexedDB

IndexedDB is a way for you to persistently store structured data , including files/blobs, inside a user’s browser. It can work both online and offline.

Usage

IndexedDB is a low-level API which means it is not easy to use. There are some wrappers of IndexedDB to improve usability, below are some:

  • localForage: A Polyfill providing a simple name:value syntax for client-side data storage, which uses IndexedDB in the background, but falls back to WebSQL and then localStorage in browsers that don’t support IndexedDB.

  • idb: A tiny (~1.15k) library that mostly mirrors the IndexedDB API, but with small improvements that make a big difference to usability.

  • Dexie.js: A wrapper for IndexedDB that allows much faster code development via nice, simple syntax.

  • idb-keyval: A super-simple-small (~600B) promise-based keyval store implemented with IndexedDB

  • sifrr-storage: A small (~2kB) promise based library for client side key-value storage. Works with IndexedDB, localStorage, WebSQL, Cookies. Can automatically use supported storage available based on priority.

  • JsStore: A simple and advanced IndexedDB wrapper having SQL like syntax.

MDN provides a full IndexedDB example:

Availability detecting

if (window.indexedDB) {
    console.log("Your browser doesn't support a stable version of IndexedDB. Such and such feature will not be available.");
}

Cache

Cache is designed to store Request / Response object pairs. It is useful in resource caching.

An origin can have multiple, named Cache objects.

Size limit

Each browser has a hard limit on the amount of cache storage that a given origin can use.

You are responsible for periodically purging cache entries. See Deleting old caches for more information.

Availability detecting

if (window.caches) {
    // ...
}

Basic usage example

Use CacheStorage.open() to open a specific named Cache object and then call any of the Cache methods to maintain the Cache.

You can access CacheStorage through the global caches property.

API

// Open a cache
// If the specified Cache does not exist, a new cache is created 
// with that cacheName and a Promise that resolves to this new
// Cache object is returned.
caches.open('my-cache').then(cache => {
    // Do things with the cache
});


// Add resource to the cache
caches.open('my-cache').then(cache => {
    fetch(url).then(function(response) {
        if (response.ok) {
            cache.put(url, response);
        }        
    });
});

// Obtain the resouce associated with an URL and add it to the cache.
// add(url) takes a URL, retrieves it, and adds the resulting response
// object to the given cache.
caches.open('my-cache').then(cache => {
    cache.add(url);
});

// Retrieve a response associated with a request
caches.open('my-cache').then(cache => {
    cache.match(url).then(res => {
        //...
    });
});

// Delete 
caches.open('my-cache').then(cache => {
    cache.delete(ur);
});

// Delete cache
caches.delete('my-cache').then(() => {
    // Deleted
});

Example

A simple example variant from MDN:

async function getCacheFirst(request) {
    var cacheAvailable = window.caches ? true : false;
    var response = await caches.match(request).catch((err) => {
        cacheAvailable = false;
        return null;
    });

    if (response) {
        return response;
    }

    // Not found in the caches or the caches are blocked.
    return await fetch(request).then(res => {
        if (cacheAvailable) {
            var res2 = res.clone();
            caches.open('v1').then((cache) => {
                cache.put(request, res2);
            });
        }

        return res;
    }).catch(err => {
        return null;
    });
}

In the code above, clone() is needed because put() consumes the response body.

Note:

The default example code from MDN has issues if not updated. It fails with TypeError or DOMException.

  • If the caches are available (not blocked by the browser), then it fails at cache.put(event.request, response) with Uncaught TypeError because the parameter response is null , it has not been saved in the cache yet.

  • If the caches are blocked by the browser (Like the cookies are blocked for the website in the settings of Chrome), it runs the first catch statement and then fails at caches.open('v1') in the then block with Uncaught DOMException because the cache is not allowed to access.

const cachedResponse = caches
  .match(event.request)
  .catch(() => fetch(event.request))
  .then((r) => {
    response = r;
    caches.open("v1").then((cache) => {
      cache.put(event.request, response);
    });
    return response.clone();
  })
  .catch(() => caches.match("/gallery/myLittleVader.jpg"));

You can download the whole simple-service-worker source code from simple-service-worker example.

MDN: Cache – Example is another example

Reference