Behansa

Beautiful, personalized wedding websites made for your love story.

Autheona

Only Let Real Users Access Your Product

Back

Browser Storages

Modern web applications require client-side storage to maintain state, cache data, and provide offline capabilities. Choosing the wrong storage API results in security vulnerabilities, performance bottlenecks, and lost data.

If you're new to tech or are a junior, read the following Jargon Decoder before continuing.

Key Terms
  • XSS (Cross-Site Scripting): An attack where malicious JavaScript is injected into the client browser, often used to steal data from local storage.
  • CSRF (Cross-Site Request Forgery): An attack that tricks an authenticated user into executing unwanted actions, typically exploiting cookie-based sessions.
  • Synchronous Blocking: Operations that halt the main thread, freezing the user interface until the read or write action completes.
  • Asynchronous I/O: Operations that run in the background, allowing the UI to remain responsive while data is being read from or written to the disk.
  • Hydration Mismatch: An error occurring in server-side rendered (SSR) applications when the server-generated HTML differs from the client-rendered HTML due to client-only data like browser storage.

The Browser Storage Landscape

You have four native options. We will evaluate them based on capacity, performance, and data integrity.

  • Local Storage: ~5MB capacity. Synchronous read/write. Stores strings only.
  • Session Storage: ~5MB capacity. Synchronous read/write. Stores strings only. Cleared on tab close.
  • Cookies: 4KB capacity. Synchronous read/write. Stores strings only. Sent to the server with every HTTP request.
  • IndexedDB: >50MB capacity (often up to gigabytes depending on disk space). Asynchronous read/write. Stores complex structured data (Objects, Blobs, Arrays).

Local and Session Storage

The Web Storage API consists of localStorage and sessionStorage. Both share the exact same methods. The only difference is persistence. localStorage persists across browser sessions. sessionStorage is destroyed when the specific browser tab closes.

When to use Local Storage

  • Saving user preferences (e.g., dark mode, UI layouts).
  • Caching non-sensitive, static API responses to reduce network payload.

When to use Session Storage

  • Storing form data during a multi-step checkout process.
  • Maintaining temporary state that should not leak across parallel tabs.

When NOT to use them

  • Never store JWTs, passwords, or PII here. Any third-party script running on your domain (analytics, ad networks, compromised NPM packages) can access this data via XSS.
  • Do not store large datasets. The API is synchronous. Writing a massive JSON string blocks the main thread and causes UI stuttering.

Implementation

export function savePreference(
  key: string,
  data: Record<string, unknown>,
): void {
  try {
    const serializedData = JSON.stringify(data);
    window.localStorage.setItem(key, serializedData);
  } catch (error) {
    console.error("Storage write failed", error);
  }
}
export function savePreference(
  key: string,
  data: Record<string, unknown>,
): void {
  try {
    const serializedData = JSON.stringify(data);
    window.localStorage.setItem(key, serializedData);
  } catch (error) {
    console.error("Storage write failed", error);
  }
}

Code PR Review

Line 1 defines a strict TypeScript signature accepting a string key and an object payload to prevent arbitrary data insertion. Line 3 uses JSON.stringify because the Web Storage API strictly accepts string values; passing an object directly results in the useless "[object Object]" string. Line 4 calls the synchronous setItem method to write the data to disk. Lines 5-7 wrap the execution in a try-catch block to handle storage quota limits or disabled storage settings in private browsing modes.

The Protocol Workhorse

Cookies are the oldest storage mechanism. Unlike Web Storage, cookies are automatically attached to outgoing HTTP requests. This makes them the definitive choice for authentication state.

When to use Cookies

  • Session identifiers and authentication tokens.
  • Server-side rendering (SSR) context flags (e.g., passing a localized language code so the server renders the correct HTML immediately).

When NOT to use Cookies

  • Client-side data caching. With a strict 4KB limit, you will exhaust the space immediately.
  • Storing data that the server does not need. Sending unnecessary cookies inflates every HTTP request payload, degrading network performance.

Security Implementation

You cannot set HttpOnly cookies via client-side JavaScript. For strict security, authentication cookies must be set by your backend server via the Set-Cookie header. However, if you must write a client-side tracking or preference cookie, you must enforce security flags manually.

export function setSecureCookie(
  name: string,
  value: string,
  days: number,
): void {
  const date = new Date();
  date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
  const expires = "expires=" + date.toUTCString();
  document.cookie = `${name}=${value};${expires};path=/;Secure;SameSite=Strict`;
}
export function setSecureCookie(
  name: string,
  value: string,
  days: number,
): void {
  const date = new Date();
  date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
  const expires = "expires=" + date.toUTCString();
  document.cookie = `${name}=${value};${expires};path=/;Secure;SameSite=Strict`;
}

Code PR Review

Line 1 defines the function signature expecting standard string key-values and a numeric lifespan. Lines 2-3 calculate the exact expiration timestamp in milliseconds. Line 4 formats this timestamp into the strict RFC-compliant UTC string required by browsers. Line 5 constructs the raw string and writes it to document.cookie. Crucially, it appends ;Secure to ensure the cookie only transmits over HTTPS, and ;SameSite=Strict to prevent the browser from sending this cookie during cross-site requests, mitigating CSRF attacks.

The Client-Side Database

IndexedDB is a transactional, NoSQL object store in the browser. It handles massive datasets without blocking the UI thread. It is notorious for having a verbose, callback-heavy API.

graph TD
  A["Web Application"] -->|Opens Connection| B["IndexedDB API"]
  B --> C["Database Instance"]
  C --> D["Object Store: users"]
  C --> E["Object Store: offline_sync"]
  D --> F["Record: {id: 1, name: Alice}"]
  E --> G["Record: {id: 99, status: pending}"]

When to use IndexedDB

  • Progressive Web Apps (PWAs) requiring full offline functionality.
  • Caching massive assets like images, audio files (as Blobs), or large analytical datasets.
  • Draft saving for complex content management systems.

When NOT to use IndexedDB

  • Simple key-value pairs (use Local Storage).
  • Sensitive data. Data is stored unencrypted on the user's hard drive and is accessible via DevTools.

Implementation

Instead of pulling in an external dependency, we will wrap the native callback API into a modern Promise-based structure.

export function openDatabase(
  dbName: string,
  version: number,
): Promise<IDBDatabase> {
  return new Promise((resolve, reject) => {
    const request = window.indexedDB.open(dbName, version);

    request.onerror = (event) =>
      reject((event.target as IDBOpenDBRequest).error);

    request.onsuccess = (event) =>
      resolve((event.target as IDBOpenDBRequest).result);

    request.onupgradeneeded = (event) => {
      const db = (event.target as IDBOpenDBRequest).result;
      if (!db.objectStoreNames.contains("documents")) {
        db.createObjectStore("documents", { keyPath: "id" });
      }
    };
  });
}
export function openDatabase(
  dbName: string,
  version: number,
): Promise<IDBDatabase> {
  return new Promise((resolve, reject) => {
    const request = window.indexedDB.open(dbName, version);

    request.onerror = (event) =>
      reject((event.target as IDBOpenDBRequest).error);

    request.onsuccess = (event) =>
      resolve((event.target as IDBOpenDBRequest).result);

    request.onupgradeneeded = (event) => {
      const db = (event.target as IDBOpenDBRequest).result;
      if (!db.objectStoreNames.contains("documents")) {
        db.createObjectStore("documents", { keyPath: "id" });
      }
    };
  });
}

Code PR Review

Line 2 instantiates a native Promise, wrapping the legacy asynchronous callback architecture. Line 3 dispatches the connection request to the browser's IndexedDB engine. Lines 5 and 7 map the native onerror and onsuccess events to the Promise's reject and resolve functions, casting the event targets to extract the core database instance. Lines 9-14 define the database schema migration logic; onupgradeneeded fires when the requested version number is higher than the client's current version, safely creating a new "documents" object store and assigning "id" as the primary index key.

Real-World Architecture

A standard modern application uses a combination of these technologies:

  1. Authentication: The server issues an HttpOnly, Secure cookie containing the session token. Client-side JS cannot read this, preventing XSS theft.
  2. UI State: The client reads localStorage.getItem('sidebar_collapsed') during initialization to render the layout instantly without querying the server.
  3. Offline Capability: A background Service Worker fetches a 50MB JSON catalog from the server and pipes it directly into IndexedDB. The application queries IndexedDB for searches, resulting in zero network latency.

The Edge Case Confessional

No API is perfect. You will encounter framework and browser-specific failures.

If you are using Server-Side Rendering (SSR) frameworks like Next.js or Nuxt, directly calling window.localStorage in your component body will crash the server environment with a ReferenceError: window is not defined. You must wrap storage access in a useEffect hook or check typeof window !== "undefined" before execution. Furthermore, reading storage on the first render can cause a Hydration Mismatch if the client value (e.g., dark mode) differs from the server-rendered HTML (e.g., light mode).

Safari's Private Browsing mode has historically been hostile to storage APIs. Depending on the exact iOS version, Safari may expose the localStorage object but immediately throw a QuotaExceededError the moment you attempt to write 1 byte of data. Your storage wrappers must always contain try-catch blocks to degrade gracefully, falling back to in-memory variables if disk writing is blocked.

Velovra

Actionable Intelligence for Marketers

Avensiana

Future-Ready Software, Built for Scale.