Browser Storage Deep Dive · Junior Engineer Edition

INDEXEDDB
EXPLAINED

TL;DR — THE ONE PARAGRAPH YOU NEED

Your browser can store data locally. But localStorage — the simple tool everyone uses — is basically a sticky note: tiny (5MB), can only hold text, and freezes your app while reading/writing. IndexedDB is the upgrade: a real database in the browser, holds gigabytes of any data, runs in the background without freezing anything, and lets you search by field. The catch? Its raw API is painful to write. So most devs use a library on top of it.

5MB
localStorage limit
GBs
IndexedDB limit
SYNC
localStorage mode
ASYNC
IndexedDB mode
01

THE PROBLEM IT SOLVES

Imagine you're building a web app — like a todo list, a notes app, or an e-commerce site. You want to remember data between page refreshes. You have a few options… but most of them kinda suck for anything beyond the simplest case.

THE STICKY NOTE ANALOGY

Think of your browser like a desk at work. localStorage is a sticky note stuck to your monitor — fast to write, easy to read, but tiny and only holds plain text. IndexedDB is a proper filing cabinet beside your desk — way more space, organized folders, you can search through it, and it doesn't interrupt what you're doing while you file something.

WITHOUT IndexedDB
localStorage
Only stores strings (JSON.stringify everything)
Hard limit: ~5-10 MB
BLOCKS the main thread — app freezes while reading
No indexes — can't search by field
No transactions — partial writes cause corrupt state
→
WITH IndexedDB
IndexedDB
Store any JS object, Blob, File, ArrayBuffer
50MB to GBs depending on browser + device
Fully async — zero UI freezing
Indexes on any field — fast queries
Transactions — all or nothing writes
REAL WORLD EXAMPLE
Linear (the project management tool) uses IndexedDB to cache your issues locally. When you refresh the page, it shows your data instantly from IndexedDB, then syncs in the background with the server. No loading spinner on refresh.
02

WHAT IS INDEXEDDB

IndexedDB is a NoSQL database built into your browser. Every modern browser ships it — Chrome, Firefox, Safari, Edge. No install needed. No backend. It lives on the user's device.

It's called "Indexed" because you can create indexes on your data — like telling the database "let me quickly find users by email" — without scanning every record.

ANALOGY — THE LIBRARY

Think of IndexedDB like a library. The library is your database. Different genres (Fiction, Science, History) are your object stores — like tables. The catalog system that lets you find books by author or title is the index. When you checkout or return books, a transaction makes sure you're counted properly — no half-checkouts. The librarian works while you browse — async, nothing blocks.

KEY CONCEPTS
🗄️
Database
Top-level container. One per origin (your site). Has a name and version number.
📦
Object Store
Like a table. Holds records as JS objects. Each record has a unique key.
🔍
Index
Fast lookup path on a field. Query by email, date, or any property.
🔒
Transaction
All-or-nothing operation. If anything fails, all changes roll back.
Async / Non-blocking
Origin-scoped (sandboxed per site)
Persistent across sessions
Works offline
No server needed
Free, built into browser
03

HOW IT WORKS

The raw API uses an event-based callback pattern (old-school JavaScript, before Promises were invented). It's verbose. Most devs wrap it with a library. But understanding the flow helps you debug issues.

RAW INDEXEDDB — OPEN A DATABASE
// 1. Open (or create) a database named "MyApp" at version 1
const request = indexedDB.open("MyApp", 1);

// 2. This runs when db is FIRST created (or version bumped)
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  // Create an object store called "users" with auto-increment keys
  const store = db.createObjectStore("users", { keyPath: "id" });
  // Add an index so we can query by email fast
  store.createIndex("by_email", "email", { unique: true });
};

// 3. Success — db is ready
request.onsuccess = (event) => {
  const db = event.target.result;
  // Now you can read/write...
};
WHY IS IT SO VERBOSE?
IndexedDB was designed in ~2010, before Promises and async/await were part of JavaScript. You're using legacy callback patterns. This is exactly why almost everyone uses a wrapper library like Dexie.js, idb, or RxDB in production.
WITH DEXIE.JS — SAME THING, MUCH NICER
// npm install dexie
import Dexie from "dexie";

// Define your database schema
const db = new Dexie("MyApp");
db.version(1).stores({
  users: "id, email, name"  // id = primary key, email+name = indexed
});

// Write a user — clean async/await
await db.users.add({ id: 1, email: "ali@example.com", name: "Ali" });

// Query by index — just like an ORM
const user = await db.users.where("email").equals("ali@example.com").first();
console.log(user.name); // "Ali"

Dexie.js wraps all the callback madness and gives you a clean promise-based API. Under the hood it's still IndexedDB — same storage, same limits, same browser support.

04

THE ALTERNATIVES

Before picking IndexedDB, understand what else exists. Each tool has a specific job.

STORAGE CAPACITY COMPARISON
Cookies
4 KB
sessionStorage
~5 MB
localStorage
~5–10 MB
Cache API
~50+ MB
IndexedDB
GBs
50 MB – GBs
Storage Size Data types Blocking? Persists? Searchable?
Cookies 4 KB String only Yes Configurable No
localStorage 5–10 MB String only Yes! Yes No
sessionStorage 5–10 MB String only Yes! Tab only No
Cache API ~50 MB+ HTTP responses No Yes No
IndexedDB 50 MB – GBs Any JS object No Yes Yes
🗒️
local Storage
Use for simple stuff
The go-to for beginners. Store user preferences, dark mode setting, a JWT token, a flag. But: it's synchronous — every read/write blocks your JavaScript until it's done. On a slow device with lots of data, this freezes your UI.
Zero setup, every browser, stupid simple API
Blocks main thread, 5MB limit, strings only, no search
🕐
session Storage
Rarely the answer
Same as localStorage but disappears when the tab closes. Useful for one-time data like form drafts during a session. Has all the same limitations: synchronous, string-only, small.
Data automatically cleaned up on tab close
Lost on refresh in some browsers, same sync/size problems as localStorage
🍪
Cookies
Server talk only
Cookies exist to communicate with your server — they automatically attach to every HTTP request. That's their job. They're terrible for storing app data: 4KB limit, sent on every request (bandwidth waste), and accessible to JavaScript by default (XSS risk).
Sent automatically to server, configurable expiry, httpOnly flag for security
Laughably small (4KB), adds overhead to every request, not for client data
📡
Cache API
Service workers
Designed for caching HTTP responses — HTML, CSS, JS files, API responses. Used by Service Workers to make PWAs work offline. Not for structured data, not for querying by field. Think of it as "save this network response for later."
Perfect for offline PWAs, caches full responses, async
Not for structured data, no query/search, clunky for app state
📁
OPFS
Advanced / new
Origin Private File System — a newer browser API that gives you something close to a real filesystem. Faster than IndexedDB for large binary data (files, images, audio). SQLite compiled to WebAssembly runs on top of OPFS for amazing performance. Still experimental for most use cases.
Faster for large binaries, enables SQLite in browser
Newer API, more complex setup, less ecosystem support
05

WHEN TO USE IT

IndexedDB isn't always the right tool. Here's the honest breakdown:

✓ Use IndexedDB when...
💾 Storing more than a few MB of data (product catalogs, user content, media)
✈️ Building an offline-first or offline-capable app (works without internet)
⚡ Caching server data client-side to skip network calls on repeat visits
🔍 Need to search/filter/query by field (find orders by date, users by status)
🗂️ Storing complex objects, files, blobs (not just strings)
🔄 Syncing app state across tabs
✗ Skip IndexedDB when...
🔑 Just storing a token, preference, or tiny config — localStorage is fine
🚀 Your app is simple and data always loads fast from the server
📡 Data needs to reach the server — use cookies or API calls
⏱️ You need data to survive only the current tab session — sessionStorage
🤯 You're a beginner and this feature isn't critical — add complexity later
THE DECISION TREE (SIMPLE VERSION)

Need to talk to server? → Cookies or API calls
Just a tiny setting (<100KB, string)? → localStorage
Gone when tab closes? → sessionStorage
Caching whole network responses? → Cache API
Large data, offline support, or real queries? → IndexedDB

06

PRACTICAL EXAMPLE

Here's what a real-world pattern looks like in React using Dexie.js — the most popular IndexedDB wrapper. This pattern is used by apps like Notion, Linear, and many offline-first tools.

STEP 1 — DEFINE YOUR DATABASE (db.js)
import Dexie from "dexie";

// Think of this like defining your tables and columns
export const db = new Dexie("TodoApp");

db.version(1).stores({
  // "++id" = auto-increment primary key
  // other fields listed = these get indexes (fast search)
  todos: "++id, title, done, createdAt"
});
STEP 2 — USE IN REACT COMPONENT (TodoList.jsx)
import { useLiveQuery } from "dexie-react-hooks";
import { db } from "./db";

function TodoList() {
  // useLiveQuery = re-renders automatically when DB changes
  const todos = useLiveQuery(() =>
    db.todos.orderBy("createdAt").toArray()
  );

  const addTodo = async (title) => {
    await db.todos.add({
      title,
      done: false,
      createdAt: Date.now()
    });
    // No setState needed — useLiveQuery re-renders automatically!
  };

  const toggleDone = async (id, currentDone) => {
    await db.todos.update(id, { done: !currentDone });
  };

  return (
    <ul>
      {todos?.map(todo => (
        <li key={todo.id} onClick={() => toggleDone(todo.id, todo.done)}>
          {todo.title}
        </li>
      ))}
    </ul>
  );
}
THE MAGIC PART
Because useLiveQuery subscribes to the database, any component that queries the same data automatically re-renders when another component writes to it — even across browser tabs. This is way harder to build with localStorage.
LIBRARY ECOSYSTEM
Recommended wrappers

Dexie.js — Most popular, clean API, React hooks support

idb — Google's thin Promise wrapper, minimal overhead

RxDB — Full reactive database, best for complex sync

PouchDB — CouchDB-compatible, built-in server sync

GOTCHAS TO KNOW
Watch out for

Safari limits — iOS Safari may clear IndexedDB when disk is low

Schema migrations — bumping db version = onupgradeneeded must handle old data

Not encrypted — data visible to other JS on the page (XSS risk)

Not shared across origins — localhost:3000 ≠ localhost:3001

07

Q & A

RxDB vs IndexedDB — what's actually different? +

IndexedDB is the raw browser API — built into every browser, low-level, callback-based. Like talking to a database with assembly language. It works, but writing it directly is painful.

RxDB is a full library built on top of IndexedDB. It adds things the raw API doesn't have:

  • Reactive queries — data changes automatically trigger re-renders (no manual polling)
  • Multi-tab sync — change in tab A instantly shows in tab B
  • Replication — built-in sync to a server (CouchDB, Firestore, REST)
  • Encryption + compression — out of the box

RxDB is overkill for most simple apps. Dexie.js is the sweet spot — thin wrapper, clean API, no heavy abstractions.

What similar tools do most developers actually use? +
ToolBest for
Dexie.jsMost common. Clean API, IndexedDB wrapper, React hooks included
PouchDBNeed CouchDB-style server sync. Offline-first with replication
idbGoogle's minimal wrapper. Tiny, no magic, just Promises
RxDBComplex reactive apps, multi-device sync needed
SQLite via WASMHeavy relational queries, fastest option (newer, more complex setup)
localStorageTiny data <5MB, simple key-value, no search needed
Is it worth using for a finance app or padel score tracker? +

Catatan keuangan (single device, basic): depends on complexity. Just storing transactions → localStorage works fine under ~500 entries. Want to filter by date, sum by category, search by merchant → Dexie.js is worth it. 10KB overhead, much better querying.

Padel Americano/Mexicano with match history: Yes, good fit. Tournament records, player rankings, match results — structured data that grows over time and needs querying ("show last 10 games", "sort by win rate"). No server needed = IndexedDB is the natural choice.

Both apps are local-first by design — user owns their data, no backend required. This is exactly the use case IndexedDB was built for. Add an Export JSON button so users can back up or move their data.

Where does the data actually live? What if I switch browsers? +

Data lives in that browser, on that device, for that origin (domain). Nowhere else.

  • Chrome → Firefox on same device: gone — different storage, no sharing
  • Same browser, incognito/private: gone on close (private mode doesn't persist)
  • Device A → Device B: gone — no sync unless you build it yourself
  • User clears browser cache/site data: gone
  • localhost:3000 vs localhost:3001: different storage — origin-scoped

Solution: build an Export / Import JSON feature. Let users download their data as a file and re-import it in a different browser or device. Simple, zero backend needed.

Will a large database slow down my app on load? +

No — IndexedDB data sits on disk and is not auto-loaded into memory when your app starts. 10,000 records in the DB doesn't mean 10,000 records loaded on page open. You fetch only what you query, when you query it.

Performance degrades only if you:

  • Query large datasets without an index (forces full table scan)
  • Write thousands of records in a single transaction
  • Show giant unPaginated lists (a UI problem, not a DB problem)

For padel/finance app scale (hundreds to low thousands of records) — zero perceptible impact. Add indexes on fields you filter/sort by (date, category, player) and you're fine.

What kinds of apps use this pattern most? +
  • Linear — caches all your issues locally, instant page loads after first visit
  • Figma — local asset cache, offline editing support
  • Notion (partial) — offline mode, local content cache
  • VS Code for Web — file system, extension state
  • Excalidraw — drawings saved entirely in the browser
  • Google Docs — "make available offline" feature
  • Any PWA (Progressive Web App) that works without internet
  • Finance / budget apps that run fully in-browser without a backend

Your padel app is exactly this category — local-first, persistent, no server required. Good fit.