10 Client-side Storage Options and When to Use Them

Share this article

Ten Client-side Storage Options

Storing and manipulating data in the browser — also known as client-side storage — is useful when it’s not necessary or practical to send it to the web server.

Situations for storing and manipulating data in the browser include:

  • retaining the state of a client-side application — such as the current screen, entered data, user preferences, etc.
  • utilities which access local data or files and have strict privacy requirements
  • progressive web apps (PWAs) which work offline

Here are ten options for storing browser data:

  1. JavaScript variables
  2. DOM node storage
  3. Web Storage (localStorage and sessionStorage)
  4. IndexedDB
  5. Cache API (don’t use AppCache!)
  6. File System Access API
  7. File and Directory Entries API
  8. cookies
  9. window.name
  10. WebSQL

This article investigates these ten different ways to store data in the browser, covering their limits, pros, cons, and the best uses of each technique.

Before we browse the options, a quick note about data persistence …

Data Persistence

In general, data you store will either be:

  1. persistent: it remains until your code chooses to delete it, or
  2. volatile: it remains until the browser session ends, typically when the user closes the tab

The reality is more nuanced.

Persistent data can be blocked or deleted by the user, operating system, browser, or plugins at any point. The browser can decide to delete older or larger items as it approaches the capacity allocated to that storage type.

Browsers also record page state. You can navigate away from a site and click back or close and re-open a tab; the page should look identical. Variables and data regarded as session-only are still available.

1. JavaScript Variables

metric comment
capacity no strict limit but browser slowdowns or crashes could occur as you fill memory
read/write speed the fastest option
persistence poor: data is wiped by a browser refresh

Storing state in JavaScript variables is the quickest and easiest option. I’m sure you don’t need an example, but …

const
  a = 1,
  b = 'two',
  state = {
    msg:  'Hello',
    name: 'Craig'
  };

Advantages:

  • easy to use
  • fast
  • no need for serialization or de-serialization

Disadvantages:

  • fragile: refreshing or closing the tab wipes everything
  • third-party scripts can examine or overwrite global (window) values

You’re already using variables. You could consider permanently storing variable state when the page unloads.

2. DOM Node Storage

metric comment
capacity no strict limit but not ideal for lots of data
read/write speed fast
persistence poor: data can be wiped by other scripts or a refresh

Most DOM elements, either on the page or in-memory, can store values in named attributes. It’s safer to use attribute names prefixed with data-:

  1. the attribute will never have associated HTML functionality
  2. you can access values via a dataset property rather than the longer .setAttribute() and .getAttribute() methods.

Values are stored as strings so serialization and de-serialization may be required. For example:

// locate <main> element
const main = document.querySelector('main');

// store values
main.dataset.value1 = 1;
main.dataset.state = JSON.stringify({ a:1, b:2 });

// retreive values
console.log( main.dataset.value1 ); // "1"
console.log( JSON.parse(main.dataset.state).a ); // 1

Advantages:

  • you can define values in JavaScript or HTML — such as <main data-value1="1">
  • useful for storing the state of a specific component
  • the DOM is fast! (contrary to popular opinion)

Disadvantages:

  • fragile: refreshing or closing the tab wipes everything (unless a value is server-rendered into the HTML)
  • strings only: requires serialization and de-serialization
  • a larger DOM affects performance
  • third-party scripts can examine or overwrite values

DOM node storage is slower than variables. Use it sparingly in situations where it’s practical to store a component’s state in HTML.

3. Web Storage (localStorage and sessionStorage)

metric comment
capacity 5MB per domain
read/write speed synchronous operation: can be slow
persistence data remains until cleared

Web Storage provides two similar APIs to define name/value pairs. Use:

  1. window.localStorage to store persistent data, and
  2. window.sessionStorage to retain session-only data while the browser tab remains open (but see Data Persistence)

Store or update named items with .setItem():

localStorage.setItem('value1', 123);
localStorage.setItem('value2', 'abc');
localStorage.setItem('state', JSON.stringify({ a:1, b:2, c:3 }));

Retrieve them with .getItem():

const state = JSON.parse( localStorage.getItem('state') );

And delete them with .removeItem():

localStorage.removeItem('state')

Other properties and methods include:

Changing any value raises a storage event in other browser tabs/windows connected to the same domain. Your application can respond accordingly:

window.addEventListener('storage', s => {

  console.log(`item changed: ${ s.key }`);
  console.log(`from value  : ${ s.oldValue }`);
  console.log(`to new value: ${ s.newValue }`);

});

Advantages:

  • simple name/value pair API
  • session and persistent storage options
  • good browser support

Disadvantages:

  • strings only: requires serialization and de-serialization
  • unstructured data with no transactions, indexing, or search
  • synchronous access will affect the performance of large datasets

Web Storage is ideal for simpler, smaller, ad-hoc values. It’s less practical for storing large volumes of structured information, but you may be able to avoid performance issues by writing data when the page unloads.

4. IndexedDB

metric comment
capacity depends on device. At least 1GB, but can be up to 60% of remaining disk space
read/write speed fast
persistence data remains until cleared

IndexedDB offers a NoSQL-like low-level API for storing large volumes of data. The store can be indexed, updated using transactions, and searched using asynchronous methods.

The IndexedDB API is complex and requires some event juggling. The following function opens a database connection when passed a name, version number, and optional upgrade function (called when the version number changes):

// connect
function dbConnect(dbName, version, upgrade) {

  return new Promise((resolve, reject) => {

    const request = indexedDB.open(dbName, version);

    request.onsuccess = e => {
      resolve(e.target.result);
    };

    request.onerror = e => {
      console.error(`indexedDB error: ${ e.target.errorCode }`);
    };

    request.onupgradeneeded = upgrade;

  });

}

The following code connects to a myDB database and initializes a todo object store (analogous to a SQL table or MongoDB collection). It then defines an auto-incrementing key named id:

(async () => {

  const db = await dbConnect('myDB', 1.0, e => {

    db = e.target.result;
    const store = db.createObjectStore('todo', { keyPath: 'id', autoIncrement: true });

  })

})();

Once the db connection is ready, you can .add new data items in a transaction:

db.transaction(['todo'], 'readwrite')
  .objectStore('todo')
  .add({ task: 'do something' })
  .onsuccess = () => console.log( 'added' );

And you can retrieve values, such as the first item:

db.transaction(['todo'], 'readonly')
  .objectStore('todo')
  .get(1)
  .onsuccess = data => console.log( data.target.result );
  // { id: 1, task: 'do something' }

Advantages:

  • flexible data store with the largest space
  • robust transactions, indexing, and search options
  • good browser support

Disadvantages:

  • a complex callback and event-based API

IndexedDB is the best option for reliably storing large quantities of data, but you’ll want to reach for a wrapper library such as idb, Dexie.js, or JsStore.

5. Cache API

metric comment
capacity depends on device, but Safari limits each domain to 50MB
read/write speed fast
persistence data remains until cleared or after two weeks in Safari

The Cache API provides storage for HTTP request and response object pairs. You can create any number of named caches for storing any number of network data items.

The API is typically used in service workers to cache network responses for progressive web apps. When a device disconnects from the network, cached assets can be re-served so a web app can function offline.

The following code stores a network response in a cache named myCache:

// cache name
const cacheName = 'myCache';

(async () => {

  // cache network response
  const stored = await cacheStore('/service.json') );
  console.log(stored ? 'stored OK' : 'store failed');

})();

// store request
async function cacheStore( url ) {

  try {

    // open cache
    const cache = await caches.open( cacheName );

    // fetch and store response
    await cache.add( url );
    return true;

  }
  catch(err) {
    return undefined; // store failed
  }

}

A similar function can retrieve an item from the cache. In this example, it returns the response body text:

(async () => {

  // fetch text from cached response
  const text = await cacheGet('/service.json') );
  console.log( text );

})();

async function cacheGet( url ) {

  try {

    const

      // open cache
      cache = await caches.open( cacheName ),

      // fetch stored response
      resp = await cache.match(url);

    // return body text
    return await resp.text();

  }
  catch(err) {
    return undefined; // cache get failed
  }

}

Advantages:

  • stores any network response
  • can improve web application performance
  • allows a web application to function offline
  • a modern Promise-based API

Disadvantages:

  • not practical for storing application state
  • possibly less useful outside progressive web apps
  • Apple has not been kind to PWAs and the Cache API

The Cache API is the best option for storing files and data retrieved from the network. You could probably use it to store application state, but it’s not designed for that purpose and there are better options.

5.5 AppCache

AppCache was the defunct predecessor to the Cache API. This isn’t the storage solution you’re looking for. Nothing to see here. Please move along.

6. File System Access API

metric comment
capacity depends on remaining disk space
read/write speed depends on file system
persistence data remains until cleared

The File System Access API allows a browser to read, write, modify, and delete files from your local file system. Browsers run in a sandboxed environment so the user must grant permission to a specific file or directory. This returns a FileSystemHandle so a web application can read or write data like a desktop app.

The following function saves a Blob to a local file:

async function save( blob ) {

  // create handle to a local file chosen by the user
  const handle = await window.showSaveFilePicker();

  // create writable stream
  const stream = await handle.createWritable();

  // write the data
  await stream.write(blob);

  // save and close the file
  await stream.close();
}

Advantages:

  • web apps can securely read and write to the local file system
  • less need to upload files or process data on a server
  • a great feature for progressive web apps

Disadvantages:

  • minimal browser support (Chrome only)
  • the API may change

This storage option excites me the most, but you’ll need to wait a couple of years before it becomes viable for production use.

7. File and Directory Entries API

metric comment
capacity depends on remaining disk space
read/write speed unknown
persistence data remains until cleared

The File and Directory Entries API provides a sandboxed file system available to a domain which can create, write, read, and delete directories and files.

Advantages:

  • could have some interesting uses

Disadvantages:

  • non-standard, incompatibilities between implementations, and behavior may change.

MDN explicitly states: do not use this on production sites. Widespread support is several years away at best.

8. Cookies

metric comment
capacity 80Kb per domain (20 cookies with up to 4Kb in each)
read/write speed fast
persistence good: data remains until it’s wiped or expires

Cookies are domain-specific data. They have a reputation for tracking people, but they’re essential for any system which needs to maintain server state — such as logging on. Unlike other storage mechanisms, cookies are (usually) passed between the browser and server on every HTTP request and response. Both devices can examine, modify, and delete cookie data.

document.cookie sets cookie values in client-side JavaScript. You must define a string with a name and value separated by an equals symbol (=). For Example:

document.cookie = 'cookie1=123';
document.cookie = 'anothercookie=abc';

Values must not contain commas, semicolons, or whitespace, so encodeURIComponent() may be necessary:

document.cookie = `hello=${ encodeURIComponent('Hello, everyone!') }`;

Further cookie settings can be appended with semi-colon separators, including:

  • ;domain=: if not set, the cookie is only available on the current URL domain. Using ;path=mysite.com would permit it on any subdomain of mysite.com.
  • ;path=: if not set, the cookie is only available in the current path and child paths. Set ;path=/ to allow any path in the domain.
  • ;max-age=: the cookie expiry time in seconds — such as ;max-age=60.
  • ;expires=: a cookie expiry date — such as ;expires=Thu, 04 July 2021 10:34:38 UTC (use date.toUTCString() to format appropriately).
  • ;secure: the cookie will only be transmitted over HTTPS.
  • ;HTTPOnly: makes cookies inaccessible to client-side JavaScript.
  • ;samesite=: controls whether another domain can access a cookie. Set it to lax (the default, shares the cookie to the current domain), strict (stops the initial cookie being sent when following a link from another domain), or none (no restrictions).

Example: set a state cookie which expires in 10 minutes and is available on any path in the current domain:

const state = { a:1, b:2, c:3 };

document.cookie = `state=${ encodeURIComponent(JSON.stringify(state)) }; path=/; max=age=600`;

document.cookie returns a string containing every name and value pair separated by a semi-colon. For example:

console.log( document.cookie );
// "cookie1=123; anothercookie=abc; hello=Hello%2C%20everyone!; state=%7B%22a%22%3A1%2C%22b%22%3A2%2C%22c%22%3A3%7D"

The function below parses the string and converts it to an object containing name-value pairs. For example:

const
  cookie = cookieParser();
  state = cookie.state;

console.log( state );
// { a:1, b:2, c:3 }


// parse cookie values
function cookieParser() {

  const nameValue = {};

  document.cookie
    .split('; ')
    .map(nv => {

      nv = nv.split('=');
      if (nv[0]) {

        let v = decodeURIComponent( nv[1] || '' );

        try { v = JSON.parse(v); }
        catch(e){}

        nameValue[ nv[0] ] = v;

      }

    })

  return nameValue;

}

Advantages:

  • a reliable way to retain state between the client and server
  • limited to a domain and, optionally, a path
  • automatic expiry control with max-age (seconds) or Expires (date)
  • used in the current session by default (set an expiry date to persist the data beyond page refreshes and tab closing)

Disadvantages:

  • cookies are often blocked by browsers and plugins (they’re generally converted to session cookies so sites continue to work)
  • clunky JavaScript implementation (it’s best to create your own cookie handler or opt for a library such as js-cookie)
  • strings only (requires serialization and de-serialization)
  • limited storage space
  • cookies can be examined by third-party scripts unless you restrict access
  • blamed for privacy invasion (regional legislation may require you to show a warning for non-essential cookies)
  • cookie data is appended to every HTTP request and response which can affect performance (storing 50Kb of cookie data, then requesting ten 1 byte files, would incur one megabyte of bandwidth)

Avoid cookies unless there’s no viable alternative.

9. window.name

metric comment
capacity varies, but several megabytes should be possible
read/write speed fast
persistence session data remains until the tab is closed

The window.name property sets and gets the name of the window’s browsing context. You can set a single string value which persists between browser refreshes or linking elsewhere and clicking back. For example:

let state = { a:1, b:2, c:3 };
window.name = JSON.stringify( state );

Examine the value using:

state = JSON.parse( window.name );
console.log( state.b );
// 2

Advantages:

  • easy to use
  • can be used for session-only data

The disadvantages:

  • strings only: requires serialization and de-serialization
  • pages in other domains can read, modify, or delete the data (never use it for sensitive information)

window.name was never designed for data storage. It’s a hack and there are better options.

10. WebSQL

metric comment
capacity 5MB per domain
read/write speed sluggish
persistence data remains until cleared

WebSQL was an effort to bring SQL-like database storage to the browser. Example code:

// create DB (name, version, description, size in bytes)
const db = openDatabase('todo', '1.0', 'my to-do list', 1024 * 1024);

// create table and insert first item
db.transaction( t => {

  t.executeSql('CREATE TABLE task (id unique, name)');
  t.executeSql('INSERT INTO task (id,name) VALUES (1, "wash cat")');

});

// output array of all items
db.transaction( t => {

  t.executeSql(
    "SELECT * FROM task",
    [],
    (t, results) => { console.log(results.rows); }
  );

});

Chrome and some editions of Safari support the technology, but it was opposed by Mozilla and Microsoft in favor of IndexedDB.

Advantages:

  • designed for robust client-side data storage and access
  • familiar SQL syntax often used by server-side developers

Disadvantages:

  • limited and buggy browser support
  • inconsistent SQL syntax across browsers
  • asynchronous but clunky callback-based API
  • poor performance

Do not use WebSQL! It hasn’t been a viable option since the specification was deprecated in 2010.

Scrutinizing Storage

The Storage API can examine space available to Web Storage, IndexedDB, and the Cache API. All browsers except Safari and IE support the Promise-based API which offers an .estimate() method to calculate the quota (space available to the domain) and usage (space already used). For example:

(async () => {

  if (!navigator.storage) return;

  const storage = await navigator.storage.estimate();

  console.log(`bytes allocated  : ${ storage.quota }`);
  console.log(`bytes in use     : ${ storage.usage }`);

  const pcUsed = Math.round((storage.usage / storage.quota) * 100);
  console.log(`storage used     : ${ pcUsed }%`);

  const mbRemain = Math.floor((storage.quota - storage.usage) / 1024 / 1024);
  console.log(`storage remaining: ${ mbRemain } MB`);

})();

Two further asynchronous methods are available:

  • .persist(): returns true if the site has permission to store persistent data, and
  • .persisted(): returns true if the site has already stored persistent data

The Application panel in browser developer tools (named Storage in Firefox) allows you to view, modify, and clear localStorage, sessionStorage, IndexedDB, WebSQL, cookies, and cache storage.

You can also examine cookie data sent in the HTTP request and response headers by clicking any item in the developer tools’ Network panel.

Storage Smorgasbord

None of these storage solutions is perfect, and you’ll need to adopt several in a complex web application. That means learning more APIs. But having a choice in each situation is a good thing — assuming you can choose the appropriate option, of course!

FAQs About Local Storage Alternatives

What can I use instead of local storage?

When looking for alternatives to local storage in web development, consider options such as session storage, cookies, and IndexedDB. Session storage provides temporary storage for the duration of a page session, while cookies are small pieces of data sent with each HTTP request and can be used for session management and storing limited data. IndexedDB offers a more robust solution for storing structured data on the client-side, making it suitable for applications requiring asynchronous data retrieval.
For more extensive data storage or when security and persistence are essential, server-side storage solutions like MySQL, PostgreSQL, MongoDB, or cloud-based databases such as Firebase, AWS DynamoDB, or Google Cloud Firestore may be preferable. Additionally, some client-side frameworks provide their own state management solutions, while service workers can cache data and assets for offline functionality, making them suitable for progressive web apps (PWAs).

When shouldn’t you use local storage?

Local storage is a versatile client-side storage solution, but there are scenarios where it may not be the most appropriate choice. Firstly, local storage is not suitable for storing sensitive or confidential information since it lacks encryption or security measures, making it vulnerable to unauthorized access. Critical data like passwords or personal identification should be stored securely on the server-side with robust security protocols.
Secondly, local storage has limited capacity, typically around 5-10 MB per domain. It’s ill-suited for applications that need to handle substantial volumes of data. In such cases, server-side databases or more powerful client-side options like IndexedDB should be considered to accommodate larger datasets.
Lastly, local storage can pose performance issues, especially when dealing with significant data sets, as it operates synchronously and can block the main thread. For performance-critical applications, using asynchronous storage solutions like IndexedDB or implementing in-memory caching may be necessary to maintain a smooth user experience.
In summary, while local storage is valuable for lightweight, non-sensitive data storage, it’s essential to assess your project’s specific requirements. For sensitive information, large datasets, or performance-critical applications, alternative storage solutions should be explored to ensure data security, scalability, and optimal user experience.

Which is better localStorage or session storage?

The choice between localStorage and sessionStorage primarily boils down to the duration of data persistence you require and your specific use case.
localStorage is the better choice when you need data to persist across browser sessions. It’s suitable for storing data like user preferences, settings, or cached resources that should remain available to the user even if they close their browser and return to the website at a later time. Its persistence and larger storage capacity make it a good fit for scenarios where long-term data retention is essential.
On the other hand, sessionStorage is ideal for data that should only be available during the current page session. When the user closes the tab or browser, the data is automatically cleared, ensuring privacy and reducing the risk of inadvertently storing unnecessary information. This makes it well-suited for managing temporary data, such as form data, shopping cart contents, or state management within a single user interaction.

What is a client-side database?

A client-side database, also known as a front-end database, is a type of database that resides and operates on the client side of a web application, typically within a user’s web browser. It is used to store and manage data on the client’s device, allowing web applications to work offline, reduce server load, and improve user experience by minimizing the need for frequent server requests. Client-side databases are commonly used in web development to enable the storage and retrieval of data directly on the user’s device.
One of the most common examples of a client-side database is IndexedDB, a low-level JavaScript API that provides a structured database for storing large amounts of data within a web browser. IndexedDB allows developers to create, read, update, and delete data, making it suitable for applications that need to store and manage substantial amounts of information offline.
Other examples of client-side databases include Web Storage (localStorage and sessionStorage) for storing smaller amounts of data, and various in-memory databases implemented in JavaScript for temporary data storage during a user’s session.
Client-side databases are particularly useful for web applications like progressive web apps (PWAs), where maintaining functionality even when the user is offline or has a limited internet connection is a priority. They complement server-side databases by providing a mechanism for storing data locally on the user’s device, reducing latency, and enhancing the user experience.

What are the different types of client-side storage?

Client-side storage in web development comes in several forms, each with its own characteristics and use cases.
One common type is Web Storage, which includes localStorage and sessionStorage. localStorage is suitable for storing small amounts of data that need to persist across browser sessions, making it useful for user preferences or settings. In contrast, sessionStorage is session-limited and stores data only for the duration of a single page session, making it ideal for temporary data such as shopping cart contents or form data needed during a user’s interaction with a webpage.
Another option is IndexedDB, a more advanced client-side database system. IndexedDB provides structured storage for managing large volumes of data on the user’s device. It supports asynchronous data retrieval, indexing, transactions, and more, making it well-suited for applications requiring complex data handling and offline functionality, like progressive web apps (PWAs).
Additionally, cookies are small pieces of data that can be stored on the client’s device and are sent with each HTTP request to the server. While less common for general data storage today, cookies are still useful for tasks like session management, user authentication, and tracking user preferences.
Each type of client-side storage has its strengths and weaknesses, and the choice depends on your specific requirements, such as data size, persistence needs, and use case.

Craig BucklerCraig Buckler
View Author

Craig is a freelance UK web consultant who built his first page for IE2.0 in 1995. Since that time he's been advocating standards, accessibility, and best-practice HTML5 techniques. He's created enterprise specifications, websites and online applications for companies and organisations including the UK Parliament, the European Parliament, the Department of Energy & Climate Change, Microsoft, and more. He's written more than 1,000 articles for SitePoint and you can find him @craigbuckler.

appcacheappcache pitfallsbrowser storagecache apiclient-side storagecookiesdata persistencedata storagefile system apiIndexedDBweb storagewebsql
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week