Introduction

Many times, we need to save data on the client side - Offline access, personalising experiences, persisting user activity, saving assets for faster access, etc.
It consists of API that allows you to store and retrieve the data on the user's machine.

Different Storage Mechanisms

Compatibility Size Datatype Pros Cons
Cookies Everywhere 4KB string Simple, Configurable, Compatible Less Secure, Limiting, Attaches to request, easily deleted
HTML5 Storage Everywhere 2.5MB to 5MB string Simple, Non Transmitted, Compatible Unstructured data, Slow Access
WebSQL (Deprecated) Chrome, Safari, Opera, Mobile Support 2.5MB to 5MB string Asynchronous, Search Speed Deprecated, Steep Learning, Predefined Schema
IndexedDB Modern Browsers 10% to 20% available space JS Object Asynchronous, Large Dataset Steep Learning, Complicated while implementing

Generally, we can use IndexedDB and use HTML5 Storage as a fallback for our applications.

Cookies

Cookies are introduced in 1994 by Netscape and are used mainly to store User info, personalisation and analytics information.

Read all cookies accessible from this location
allCookies = document.cookie;

Write a new cookie
document.cookie = newCookie;

Please refer the below example

<!DOCTYPE html>
<html>
  <body>
    List of Cookies:
    <div id="cookie-data"></div>
    <script>
      document.getElementById("cookie-data").innerHTML = document.cookie;//.split('; ')[0];
      // Set date to tomorrow
      var date = new Date();
      var milliSecondsInDay = 86400000;
      date.setTime(date.getTime()+milliSecondsInDay);
      document.cookie = "myData=cookie2;expires=" +  date.toGMTString();
    </script>
  </body>
</html>

HTML5 Web Storage

It introduced in 2012, HTML5 Web Storage specification contains two new objects - Session Storage and Local Storage
Session storage - Clears when the browser session ends
Local storage - no expiration date and browser controlled

Example of using Session Storage:

<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
    <div id="data"></div>
    <script>
      var stamp = {'name':'MK Gandhi'};
      sessionStorage.setItem('stamp', JSON.stringify(stamp));
      
      var obj = JSON.parse(sessionStorage.stamp);
      document.getElementById("data").innerHTML = obj.name;
      sessionStorage.remove('stamp');
      sessionStorage.clear();
      
    </script>
  </body>

</html>

Example of using Local Storage:

<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
    <div id="data"></div>
    <script>
      var stamp = {'name':'MK Gandhi'};
      localStorage.setItem('stamp', JSON.stringify(stamp));
      
      var obj = JSON.parse(localStorage.stamp);
      document.getElementById("data").innerHTML = obj.name;
      localStorage.remove('stamp');
      localStorage.clear();
      
    </script>
  </body>

</html>

WebSQL (Deprecated)

It stores data that can be queried using a variant of SQL and is compatible with the most mobile browser and is fallback for other strategies.

Example using WebSQL:

<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
    <div id="data"></div>
    <script>
      // Open or Create a database with name, version, displayname, size
      var db = openDatabase('stampsLover', '1.0', 'Stamps<3', 2 * 1024 * 1024);
      db.transaction(function (tx) {
        tx.executeSql('CREATE TABLE IF NOT EXISTS stamps (id unique, officename, state)');
        tx.executeSql('INSERT INTO stamps (id, officename, state) VALUES (1, "PO Rohini", "Delhi")');
        
      });
      
      db.transaction(function (tx) {
        tx.executeSql('SELECT officename FROM stamps WHERE id=?', [1], 
          function(tx, results){ 
            console.log(results.rows.item(0));
            document.getElementById('data').innerHTML = results.rows.item(0).officename;
          });
        tx.executeSql('DROP TABLE stampsLover');
      });
      
    </script>
  </body>

</html>

Indexed DB

IndexedDB is a way for you to store data inside a user's browser persistently. Because it lets you create web applications with rich query abilities regardless of network availability, your applications can work both online and offline.

There are primarily four events:

  1. Success - On Success
  2. Error - On error
  3. UpgradeNeeded - Triggered when the user open DB and version is changed
  4. Blocked - when we have another open connection with the database.

What is ObjectStore?

ObjectStore is the data stored in the database in the form of key-value pair and must have a unique name.

There are two transactions

  1. Read-only
  2. Read-write
<!DOCTYPE html>
<html>
  <head>
  </head>

  <body>
    <div id="data"></div>
    <script>

      // In the following line, you should include the prefixes of implementations you want to test.
      // Below line considers various browsers implementation of IndexedDB
      window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
      // DON'T use "var indexedDB = ..." if you're not in a function.
      // Moreover, you may need references to some window.IDB* objects:
      window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction 
      || {READ_WRITE: "readwrite"};
      window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;
      
      // Let us open our database
      if (!window.indexedDB) {
        window.alert("Your browser doesn't support a stable version of IndexedDB. Feature not available.");
        
      }
      var request = window.indexedDB.open("StampsLover", 1);
      
      request.onerror = function(event) {
        // Do something with request.errorCode!
        // Generic error handler for all errors targeted at this database's
        // requests!
        alert("Database error: " + event.target.errorCode)
      };
      
      request.onblocked = function(event) {
        // If some other tab loads with the database, then it needs to be closed
        // before we can proceed.
        alert("Please close all other tabs with this site open!");
      };
      
      request.onupgradeneeded = function(){
        var objectStore = request.result.createObjectStore("stamps", {keyPath: "id"});
        objectStore.add({ id:1, name:'PO Rohini', state:'Delhi' });
        objectStore.add({ id:2, name:'PO Karol Bagh', state:'Delhi' });
        objectStore.createIndex('by-name', 'name');
      }
      
      request.onsuccess = function(){
        var transaction = request.result.transaction(["stamps"], "readonly");
        var objectStore = transaction.objectStore("stamps");
        objectStore.get(1).onsuccess = function(){
          document.getElementById("data").innerHTML = this.result.name;
        }
      }
      
    </script>
  </body>

</html>

Application cache (deprecated)

It stores all pages and loads them from the cache when requested.

  1. We define what files needs to be cached and what files need to access the network and fallback pages.

  2. We can either update using programmatically - windows.ApplicationCache object or modify the manifest file itself

Example using Application Cache

  1. Enabling the application cache: Including manifest attribute in html tag.
    <html manifest="example.appcache">
  2. Defining appcache file
    Three sections in a cache manifest file:
    CACHE, NETWORK, and FALLBACK
CACHE MANIFEST
# v1 2011-08-14
# Files listed are cached explicitly after first load
index.html
cache.html
style.css
image1.png

# Use from network if available
# All requests to such resources bypass the cache, even if the user is offline
NETWORK:
network.html

# Fallback content
# Fallback pages the browser should use if a resource is inaccessible
FALLBACK:
. fallback.html

Use the manifestR tool to get your sites offline. It create an HTML5 appcache manifest file for that page. http://westciv.com/tools/manifestR/

Cache API

Cache object where request objects act as keys to their responses. Here we precache our objects and loads them.

Service Worker

Let's go over service worker specs and use the network as a spec. Service worker is scripts that work separately from a webpage and runs in a separate thread.

  1. Intercept network requests
  2. Functional Events (fetch, push, sync)
  3. Proxy between networks and the browsers and work even when browsers are closed

Basic Service Worker Sample: https://github.com/GoogleChrome/samples/tree/gh-pages/service-worker

What It Does

  • Precaches the HTML, JavaScript, and CSS files needed to display this page offline. (Try it out by reloading the page without a network connection!)
  • Cleans up the previously precached entries when the cache name is updated.
  • Intercepts network requests, returning a cached response when available.
  • If there's no cached response, fetches the response from the network and adds it to the cache for future use.
/*
 Copyright 2016 Google Inc. All Rights Reserved.
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
     http://www.apache.org/licenses/LICENSE-2.0
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
*/

// Names of the two caches used in this version of the service worker.
// Change to v2, etc. when you update any of the local resources, which will
// in turn trigger the install event again.
const PRECACHE = 'precache-v1';
const RUNTIME = 'runtime';

// A list of local resources we always want to be cached.
const PRECACHE_URLS = [
  'index.html',
  './', // Alias for index.html
  'styles.css',
  '../../styles/main.css',
  'demo.js'
];

// The install handler takes care of precaching the resources we always need.
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(PRECACHE)
      .then(cache => cache.addAll(PRECACHE_URLS))
      .then(self.skipWaiting())
  );
});

// The activate handler takes care of cleaning up old caches.
self.addEventListener('activate', event => {
  const currentCaches = [PRECACHE, RUNTIME];
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));
    }).then(cachesToDelete => {
      return Promise.all(cachesToDelete.map(cacheToDelete => {
        return caches.delete(cacheToDelete);
      }));
    }).then(() => self.clients.claim())
  );
});

// The fetch handler serves responses for same-origin resources from a cache.
// If no response is found, it populates the runtime cache with the response
// from the network before returning it to the page.
self.addEventListener('fetch', event => {
  // Skip cross-origin requests, like those for Google Analytics.
  if (event.request.url.startsWith(self.location.origin)) {
    event.respondWith(
      caches.match(event.request).then(cachedResponse => {
        if (cachedResponse) {
          return cachedResponse;
        }

        return caches.open(RUNTIME).then(cache => {
          return fetch(event.request).then(response => {
            // Put a copy of the response in the runtime cache.
            return cache.put(event.request, response.clone()).then(() => {
              return response;
            });
          });
        });
      })
    );
  }
});

Summary

Cookies - Limiting
HTML5 Local Storage - Limited Size
WebSQL - Deprecated
IndexedDB - Limited Support (desktop)

We generally, plan for IndexedDB and when not possible downgrade to HTML5 Local storage.
Instead of writing code on our own, we should use a third-party library to help with offline storage since the overhead of making client storage is verbose because IndexedDB is without promises.
We can use Mozilla's localForage library. After that we introduce Service Worker to control the Caching strategies.