Offline Storage

There were several iterations of prototypes and standardized technologies for offline storage capabilities for Web applications. First attempts were either just hacky workarounds (like to store data in cookies) or required additional software (like Flash or Google Gears). Later, Web SQL idea, basically to include SQLite natively within a browser, was coined and implemented throughout some browsers, but deprecated later due to the standardization difficulties.

Right now there are at least three distinct and independent technologies standardized and available. The simplest one is Web Storage - a key-value string storage, allowing Web applications to store data either persistently and cross-window (localStorage) or for a single session in a single browser tab (sessionStorage). The more sophisticated IndexedDB is a low-level API over database-like structures with transactions and cursors iterating by indexes. The newest addition - Cache API is a specialized solution to keep Request/Response pairs, useful mostly within Service Worker implementation.

Live example and usage data shown here are referring to Web Storage engine. For details on IndexedDB, refer to caniuse.com.

The actual persistence of data stored in any of the persistent stores (be it localStorage, IndexedDB or Cache API) is browser-managed and by default might be wiped out without end-user consent in case of memory pressure conditions. To address this problem, Storage API was introduced - it gives the Web applications a method to store the data in a fully reliable way if the user permits it to do so. Chrome's implementation grants this permission based on user-engagement-based heuristic, while Firefox asks for the permission explicitly.

API glimpse

Web Storage API

window.sessionStorage
Gives an access to the Web Storage engine with per-session objects lifetime.
window.localStorage
Gives an access to the Web Storage engine with persistent objects lifetime.
storage.setItem(key, value)
Saves the value string under the key in the selected storage engine.
storage.getItem(key)
Returns the string value stored under the key in the selected storage engine.
storage.removeItem(key)
Removes the string value stored under the key from the selected storage engine.
storage.clear()
Removes all the string values stored in the selected storage engine.
window.addEventListener('storage', listener)
An event fired when the data stored in either sessionStorage or localStorage has been changed externally.

IndexedDB

let openRequest = window.indexedDB.open(name, version)
Triggers opening a database connection to either existing or newly-created database. Returns an object that fires success event when the connection is established.
let db = openReques.result
Gives an access to the open database connection instance - available after success was fired.
db.createObjectStore(storeName, options)
Creates a named container (object store) for objects in the opened database.
let tx = db.transaction(storeName)
Opens a data-reading or data-manipulation transaction scoped to the given object store(s).
tx.objectStore.put(value, key)
Saves the value in the currently opened object store.
tx.objectStore.get(key)
Gets the object stored under a key in the currently opened object store.
tx.createIndex(name, keyPath, options)
Creates an index that allows to seek for the stored objects using the property specified via keyPath.
tx.index(name).get(key)
Gets the object having the particular index keyPath equal to the key specified.

Cache API

let cache = window.caches.open(key)
Returns a Promise that resolves to a store "bucket" object giving an access to the cached Response objects.
cache.put(request, response)
Saves the Response object to the cache with its corresponding Request object.
cache.match(request, option)
Returns a Promise that resolves to the Response object matching the specified Request (with the options-controlled level of exactness) found in the opened cache "bucket".
cache.delete(request, option)
Removes the Response object matching the specified Request (with the options-controlled level of exactness) found in the opened cache "bucket".

Storage API (persistence permission)

navigator.storage.persist()
Requests a permission to turn the data saved by the Web application into persistent data. Returns a Promise that resolves with a boolean value indicating whether the permission was granted.
navigator.storage.persisted()
Returns a Promise that resolves with a boolean value indicating whether the persistent storage permission was already granted.

Live Demo

Open the example in another tab and change the value there to see the synchronization via storage event.

  • if ('localStorage' in window || 'sessionStorage' in window) {
      var selectedEngine;
    
      var logTarget = document.getElementById('target');
      var valueInput = document.getElementById('value');
    
      var reloadInputValue = function () {
        valueInput.value =  window[selectedEngine].getItem('myKey') || '';
      }
      
      var selectEngine = function (engine) {
        document.querySelector('[data-engine=' + engine + ']').classList.add('active');
        if (selectedEngine) {
          document.querySelector('[data-engine=' + selectedEngine + ']').classList.remove('active');
        }
    
        selectedEngine = engine;
        reloadInputValue();
      };
    
      var getSelectEngineFn = function (button) {
        return function () {
          var engine = button.getAttribute('data-engine');
          if (selectedEngine !== engine) {
            selectEngine(engine);
          }
        };
      };
    
      function handleChange(change) {
        var timeBadge = new Date().toTimeString().split(' ')[0];
        var newState = document.createElement('p');
        newState.innerHTML = '<span class="badge">' + timeBadge + '</span> ' + change + '.';
        logTarget.appendChild(newState);
      }
    
      var buttons = document.querySelectorAll('#selectEngine button');
      for (var i = 0; i < buttons.length; ++i) {
        buttons[i].addEventListener('click', getSelectEngineFn(buttons[i]));
      }
      
      selectEngine('localStorage');
    
      valueInput.addEventListener('keyup', function () {
        window[selectedEngine].setItem('myKey', this.value);
      });
    
      var onStorageChanged = function (change) {
        var engine = change.storageArea === window.localStorage ? 'localStorage' : 'sessionStorage';
        handleChange('External change in <b>' + engine + '</b>: key <b>' + change.key + '</b> changed from <b>' + change.oldValue + '</b> to <b>' + change.newValue + '</b>');
        if (engine === selectedEngine) {
          reloadInputValue();
        }
      }
    
      window.addEventListener('storage', onStorageChanged);
    }
  • <p>
      <label>Engine</label>
    </p>
    <div class="btn-group" role="group" id="selectEngine">
      <button type="button" class="btn btn-default" data-engine="localStorage">Persistent Storage</button>
      <button type="button" class="btn btn-default" data-engine="sessionStorage">Per-Session Storage</button>
    </div>
    
    <p>
      <label for="value">Value for <code>myKey</code></label>
      <input type="text" class="form-control" id="value">
    </p>
    
    <p>Open the example in another tab and change the value there to see the synchronization via <code>storage</code> event.</p>
    <div id="target"></div>

Resources