Pick localStorage for a few small settings: it stores strings only, about 5 MiB per origin, and every call blocks the page until it finishes. Pick IndexedDB for real data: it is asynchronous, transactional, stores objects, dates and files, and its quota is a share of the disk. The demo below writes one note to both.
| Question | localStorage | IndexedDB |
|---|---|---|
| What can it hold? | Strings only | Anything the structured clone algorithm copies, including Blob and File |
| Size | Up to 5 MiB per origin | A share of free disk, decided per browser |
| Blocks the page? | Yes, every call is synchronous | No, results arrive by events or promises |
| Transactions and indexes | No | Yes |
| Usable in a Web Worker | No, it is a property of window | Yes |
| Best for | Theme, last tab, a draft string | Offline data, caches, files, anything you query |
Updated
Both APIs keep data in the visitor's browser after the tab closes, and both are scoped to the page's origin. The difference is weight. localStorage is a tiny synchronous string map you can learn in a minute; IndexedDB is a small database with versions, object stores and transactions. This page shows the real limits from the documentation and lets you watch what each one returns.
This demo runs only in your browser. It writes under the key we-demo-note and a database called we-demo-db, and the Clear button removes both.
Note to save: Call the printer on Friday
Nothing read yet. Save a note, then press Read back.
Figures as documented on MDN's storage quotas page. IndexedDB, the Cache API and the origin private file system share the same quota.
| Browser | localStorage | IndexedDB (best-effort) | IndexedDB (persistent) |
|---|---|---|---|
| Chrome, Edge and other Chromium | 5 MiB | Up to 60% of total disk | Up to 60% of total disk |
| Firefox | 5 MiB | Smaller of 10% of disk or 10 GiB per site group | Up to 50% of disk, capped at 8 TiB |
| Safari browser app | 5 MiB | About 60% of total disk | About 60% of total disk |
| Apps that embed web content (WebKit) | 5 MiB | About 15% of total disk | About 15% of total disk |
The try...catch handles a full store or blocked storage. The date has to be rebuilt by hand after parsing.
function saveNote(text) {
const note = { text, savedAt: new Date().toISOString() };
try {
localStorage.setItem("note", JSON.stringify(note));
return true;
} catch (err) {
console.warn("Could not save:", err.name); // QuotaExceededError or SecurityError
return false;
}
}
function loadNote() {
const raw = localStorage.getItem("note"); // a string or null
if (!raw) return null;
const note = JSON.parse(raw);
return { ...note, savedAt: new Date(note.savedAt) };
}onupgradeneeded runs the first time and whenever the version number goes up; that is the only place to create object stores.
function openDb() {
return new Promise((resolve, reject) => {
const req = indexedDB.open("notes-db", 1);
req.onupgradeneeded = () => {
req.result.createObjectStore("notes", { keyPath: "id" });
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
async function saveNote(text) {
const db = await openDb();
return new Promise((resolve, reject) => {
const tx = db.transaction("notes", "readwrite");
tx.objectStore("notes").put({ id: "latest", text, savedAt: new Date() });
tx.oncomplete = () => resolve(true);
tx.onerror = () => reject(tx.error);
});
}estimate() returns approximate byte counts. persist() resolves to true if the browser granted persistent storage.
const { usage, quota } = await navigator.storage.estimate();
console.log(`Using ${(usage / 1e6).toFixed(1)} MB of ${(quota / 1e9).toFixed(1)} GB`);
if (navigator.storage.persist) {
const kept = await navigator.storage.persist();
console.log(kept ? "Data will not be evicted automatically" : "Best-effort storage");
}
When code calls localStorage.getItem, the browser stops running other JavaScript until the value comes back from disk. For a theme name nobody notices. For a few megabytes of JSON parsed on every page load, it shows up as a pause before the page responds to taps.
IndexedDB hands back a request object straight away and fires success or error later. That shape is more code to write, which is why most projects use a promise wrapper, but it never holds the page hostage.
localStorage turns everything into text. Store the number 3 and you read back the string "3"; store an object without JSON.stringify and you read back "[object Object]".
IndexedDB copies values with the structured clone algorithm, so a Date stays a Date, a Map stays a Map, and a Blob or File can be saved as it is. That is what makes it suitable for offline images, recorded audio or drafts with attachments.
If the data fits in one short line and you read it once at start-up, localStorage is fine. If you would want to search it, count it, page through it, store binary files in it or update it from a service worker, use IndexedDB.
Server data is a third option that often beats both: if the person expects to see the same thing on another device, it belongs in your database, with the browser store only as a cache.
MDN documents a limit of 5 MiB of local storage and 5 MiB of session storage per origin, 10 MiB in total. Because values are UTF-16 strings, the number of characters you fit can be lower than you expect. Going over the limit throws a QuotaExceededError.
It depends on the browser and the disk. Chromium browsers allow an origin up to 60% of total disk size. Firefox allows the smaller of 10% of the disk or 10 GiB per site group in best-effort mode. Safari browser apps allow about 60% of the disk. Use navigator.storage.estimate() to read the real figure.
For a single tiny value, localStorage is usually quicker to write in code and fast enough to run. The problem is that it is synchronous, so large reads and writes freeze the main thread. IndexedDB does the work off the main flow and tells you when it finishes, which keeps scrolling and typing smooth.
Yes. Storage is best-effort by default, so the browser may evict an origin's data when the disk is under pressure. Asking navigator.storage.persist() switches to persistent mode, where only the person can clear it. Firefox shows a permission prompt; Chrome and Safari decide from how the person uses the site.
No. Any script on the page, including an injected one, can read localStorage and IndexedDB. Keep session identifiers in cookies marked HttpOnly, Secure and SameSite, which page scripts cannot read.
Related: localStorage in JavaScript, CORS explained, JSON formatter, Data URLs explained.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.