Engineering 20 min read

IndexedDB vs OPFS: Browser Storage Benchmarked in 2026

MMNMNOTE
indexeddbopfsbrowser-storagelocal-firstweb-performancebenchmarkjavascript

Reference: WHATWG File System Standard · MDN: Origin private file system — the specs behind OPFS · IndexedDB per the W3C Indexed Database API.

There is no single fastest browser storage engine. In 2026, IndexedDB wins tiny single-document writes and reads; OPFS, driven through a synchronous access handle inside a Web Worker, wins bulk and large-file I/O by roughly two to four times 1 2. Pick per access pattern, and measure your own corpus.

That answer disappoints people who want a leaderboard. But a browser-based, local-first app — one that keeps your notes on your own device and works offline — lives or dies on this substrate, and the honest engineering result is a decision rule, not a winner. This post benchmarks the two dominant on-device stores, explains why each is fast where it is fast, shows the code, and ends with the trade-off nobody benchmarks: durability. Every number here is a dated snapshot from a named source, tied to a specific access pattern.

What are IndexedDB and OPFS?

IndexedDB is a transactional, indexed key-value database built into every browser. OPFS — the origin private file system — is a sandboxed, high-performance file store reachable through the File System API 3. IndexedDB stores structured records you query; OPFS stores raw bytes you read and write in place, off the main thread.

The distinction is architectural, not cosmetic. IndexedDB gives you object stores, indexes, key ranges, and cursors — a database. You put JavaScript objects in and query them out. OPFS gives you a private directory tree of files and a handle onto their bytes. MDN describes it as a store that "is private to the origin of the page and not visible to the user like the regular file system" 3. There is no query language and no index. If IndexedDB is a small embedded database, OPFS is a fast local disk with no filesystem browser attached. Choosing between them is really choosing what shape your data wants to be.

Is IndexedDB actually slow?

IndexedDB is slower than developers expect, but usually because of how it is used, not what it is. RxDB warns that inserting a few hundred documents unbatched can take several seconds 4. The fix is fewer, larger transactions and bulk reads through getAll() — not abandoning the API.

The RxDB team is blunt about the reputation: "IndexedDB is slow. Not slow like a database on a cheap server, even slower!" 4. The cost is not the storage engine so much as the ceremony around it — every operation rides a transaction, and a naive cursor walk pays idle time between each row. Nolan Lawson, who worked on PouchDB and later at Microsoft Edge, spent a canonical article dissecting exactly this and lands on a pragmatic verdict: "For better or worse, IndexedDB is here to stay." 5 His 2025 update softens the picture further: "there is now a getAllRecords API in IndexedDB which resolves some of the issues described in this post." 5

The practical rewrite is small. Replace a per-row cursor with a single bulk read inside one transaction:

// IndexedDB: one transaction, one getAll() — not a cursor per row
function readAll(db, storeName) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(storeName, "readonly");
    const req = tx.objectStore(storeName).getAll();
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

Most "IndexedDB is slow" benchmarks are really measuring transaction overhead multiplied by row count. Batch the writes, bulk the reads, and the API is far closer to its ceiling than its reputation suggests.

How OPFS gets its speed

OPFS is fast for one structural reason: it exposes a synchronous file handle. Call createSyncAccessHandle() inside a dedicated Web Worker and you get blocking read(), write(), and flush() straight onto bytes, with no transaction machinery and no main-thread hops 3 6. The catch is the whole sentence — it only works inside a Worker.

MDN puts the performance claim plainly: OPFS "provides access to a special kind of file that is highly optimized for performance and offers in-place write access to its content." 3 The WHATWG File System Standard explains the design intent behind the synchronous handle: "The returned FileSystemSyncAccessHandle offers synchronous methods. This allows for higher performance on contexts where asynchronous operations come with high overhead, e.g., WebAssembly." 6 Synchronous, in-place, no promise per byte — that is where the speed comes from.

The constraint is real and non-negotiable. The synchronous methods, RxDB notes, "cannot be used in the main thread, an iFrame or even a SharedWorker" 1. So OPFS at full speed means shipping a dedicated Worker:

// opfs-writer.worker.js — runs off the main thread
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle("notes.bin", { create: true });

// Despite the name, createSyncAccessHandle() itself is async to obtain;
// the returned handle's read/write/flush are synchronous.
const access = await fileHandle.createSyncAccessHandle();

const bytes = new TextEncoder().encode(payload);
access.write(bytes, { at: access.getSize() }); // append, in place
access.flush();
access.close();

That worker boundary is the price of admission. You serialize data across postMessage, you manage a second execution context, and you lose the convenience of touching storage from a click handler. In exchange you get the fastest durable write path the browser offers — provided your access pattern actually benefits.

The benchmark: OPFS vs IndexedDB by access pattern

Across the freshest 2026 numbers, the ranking flips with the workload. For small single-document writes IndexedDB is faster — 0.17 ms versus 1.54 ms in RxDB's suite; for bulk reads and large blobs OPFS pulls ahead by two to four times 2 1. Neither is universally faster. The access pattern decides the winner every time.

Here is the picture assembled from RxDB's own measurements (their comparison article, last updated June 2026, and their OPFS storage docs) alongside Autodesk's production caching results. Treat every figure as a dated snapshot from that source, on that source's hardware and browser versions — not an independent lab result.

WorkloadIndexedDBOPFS (Worker sync handle)Source
Small single-doc write (latency)~0.17 ms~1.54 msRxDB comparison, upd. 2026-06-30 2
Small single-doc read (latency)~0.1 ms~1.41 msRxDB comparison 2
Store init time~46 ms~23 ms (main)RxDB comparison 2
Plain inserts (new file per write)baseline"up to 2x times faster"RxDB OPFS docs 1
Reads, incl. complex queriesbaseline"up to 4x faster"RxDB OPFS docs 1
Bulk read in Worker vs main thread"about twice as fast"RxDB comparison 2
Real-world cache load (IDB→OPFS)baseline1.1–1.7x (small) · 2–7x (medium) initialAutodesk APS, 2024-03-07 7

Read the two halves against each other. On a single small record, IndexedDB is roughly nine times quicker per operation, because a tiny keyed write is exactly what a transactional store is tuned for and OPFS pays fixed per-call overhead 2. Flip to bulk and binary and OPFS reverses it: RxDB records reads "up to 4x faster compared to IndexedDB, even with complex queries" 1, and Autodesk, after switching its viewer's geometry cache from IndexedDB to OPFS, reported initial model loads improving "1.1-1.7x for small caches, about 2-7x for … medium-sized cache" and subsequent loads "2x-4x … small caches, 3-6x … medium-sized caches" 7. The multiplier is not a property of OPFS. It is a property of the workload you hand it.

One nuance hides inside the OPFS numbers: file granularity. RxDB found that "OPFS is slow in writing one file per document," and that appending to a single consolidated file changes the performance pattern significantly 2. A thousand tiny files is a thousand handle opens; one packed file is one. If you adopt OPFS, your data layout matters as much as the API.

Durability and quota: what benchmarks ignore

Speed is the wrong question if your data disappears. Both IndexedDB and OPFS share one origin quota — up to 60% of disk on Chromium — and both are evictable. Under storage pressure the browser deletes the least-recently-used origin's data unless you call navigator.storage.persist() 8. Benchmarks almost never mention this, and it matters more than any multiplier.

The quota is generous and browser-specific. MDN documents that on Chromium-based browsers "an origin can store up to 60% of the total disk size in both persistent and best-effort modes," while Firefox caps best-effort mode at the smaller of 10% of disk or a 10 GiB per-site group limit, and lifts persistent origins to 50% of disk (capped at 8 TiB) 8. Crucially, OPFS and IndexedDB do not get separate budgets — they draw from the same per-origin pool. Choosing OPFS does not buy you more space.

Eviction is the sharp edge. MDN is explicit: "The data from the least recently used origin is deleted. If storage pressure continues, the browser moves on to the second least recently used origin, and so on." 8 That policy "skips over origins that have been granted data persistence by using navigator.storage.persist()" 8. For anything a user would be upset to lose, requesting persistence is not optional:

// Ask the browser to protect this origin from LRU eviction
if (navigator.storage?.persist) {
  const persisted = await navigator.storage.persist();
  const { usage, quota } = await navigator.storage.estimate();
  console.log({ persisted, usage, quota });
}

There is a privacy note worth stating precisely. OPFS is "not visible to the user" and "permission prompts and security checks are not required to access files in the OPFS" 3 — which is convenient, but it does not mean the bytes are encrypted or permanent. They sit on the user's disk in the clear, and they can be evicted. Fast, private-to-the-origin, and durable are three different properties; do not let a benchmark blur them.

Can you run SQLite in the browser on OPFS?

Yes. SQLite shipped a WebAssembly build in version 3.40 (November 2022), and an OPFS-backed virtual file system makes that database persistent 9. It runs only in a Worker, and the classic OPFS VFS also needs SharedArrayBuffer plus COOP and COEP response headers. You get real SQL over durable, on-device browser storage.

The 3.40.0 release notes are careful about scope: "Add support for compiling SQLite to WASM and running it in web browsers." 9 Persistence came through the WASM subproject's OPFS VFS, which comes in two flavors — a classic opfs VFS that requires cross-origin isolation, and an opfs-sahpool VFS built on sync access handles that avoids the SharedArrayBuffer requirement. Both must run in a Worker, because that is the only place the synchronous handle lives. This is the third option in the storage decision: when your data is relational and you genuinely need JOINs and WHERE clauses, a WASM SQLite database on OPFS gives you a real query engine — at the cost of shipping a multi-hundred-kilobyte WASM binary and wiring up isolation headers.

A reproducible benchmark protocol

Vendor multipliers are dated snapshots tied to specific hardware, browser versions, and access patterns. The only number you should trust is the one from your own corpus. A minimal protocol: insert N documents, read them all back, bulk-write one large blob, and time each store the same way, in the same browser, on the same device.

The harness is deliberately boring. Time whole batches, not single calls, and report per-operation averages so the fixed overheads wash out:

async function bench(label, op, n) {
  const start = performance.now();
  for (let i = 0; i < n; i++) await op(i);
  const total = performance.now() - start;
  console.log(`${label}: ${(total / n).toFixed(3)} ms/op over ${n} ops`);
  return total / n;
}

// Run the SAME N against each store: IndexedDB (main thread),
// OPFS sync handle (in a Worker), and SQLite-WASM on OPFS.
// Vary N across 100 / 1,000 / 10,000 and vary payload size —
// the crossover point moves with both.

Run it across at least two orders of magnitude of N and two payload sizes. You will watch the winner change under your own eyes: IndexedDB ahead on small keyed writes, OPFS ahead as records get bigger or reads get bulkier. That crossover is the only benchmark that describes your app, and it is why the honest answer to "which is faster" is always "measured how?"

Which should a local-first app use?

Use IndexedDB as the default for structured notes and metadata — indexed queries, transactions, and tiny writes are its home turf. Reach for OPFS in a Worker when you move large binary blobs or read thousands of records at once. Use SQLite-WASM on OPFS only when you need real SQL. These can coexist in one app.

flowchart TD
  A[What is the<br/>access pattern?] --> B{Structured records<br/>you query?}
  B -->|Yes, small writes| C[IndexedDB<br/>on main thread]
  B -->|No| D{Large blobs or<br/>bulk read-heavy?}
  D -->|Yes| E[OPFS sync handle<br/>in a Web Worker]
  D -->|Need real SQL| F[SQLite-WASM<br/>on OPFS in a Worker]
  C --> G[Call persist<br/>to resist eviction]
  E --> G
  F --> G

Figure: A storage decision tree. Start from the access pattern. Structured records with small writes go to IndexedDB on the main thread. Large blobs or bulk read-heavy work go to an OPFS synchronous access handle inside a Web Worker. Relational needs go to SQLite-WASM on OPFS, also in a Worker. Whatever the leaf, call navigator.storage.persist() so the browser does not evict the data under storage pressure.

Most real local-first apps end up using more than one leaf of that tree: IndexedDB for the note index and metadata a user edits constantly, OPFS for attachments and large exports, persistence requested on top of both. The support floor is comfortable — createSyncAccessHandle() has been available across browsers since March 2023 and sits at roughly 93% global support today, having shipped in Chrome and Edge 102 and Firefox 111 10. You are not betting on a fringe API. You are choosing which mature one fits each job.

Trade-offs and what I would steal

The honest trade-offs are symmetric. OPFS gives you speed and raw bytes but no indexes, no queries, and a mandatory Worker. IndexedDB gives you queries and main-thread access but a callback-heavy API and real per-transaction overhead. The reusable idea is to match the store to the access pattern, not to crown a single winner.

Three ideas travel beyond this comparison. First, the sync handle is the whole trick — OPFS is fast because it removes the async tax, and the price is the Worker boundary; whenever an API offers a synchronous fast path gated behind a thread, that gate is the design, not an accident. Second, data layout beats API choice — RxDB's "one file per document is slow, one packed file is fast" result 2 is a reminder that how you shard your bytes usually dominates which store holds them. Third, durability is a separate axis from speed — the fastest write is worthless if an LRU sweep deletes it, so persist() and estimate() belong in the same code path as the write itself 8. Benchmark the speed; design for the eviction.

The verdict: IndexedDB remains the correct default for structured, queryable, frequently-edited data, and its "slowness" is mostly fixable with batching and getAll(). OPFS is the right tool the moment your workload turns bulk or binary, and it is now supported widely enough to depend on. A serious local-first app in 2026 does not choose between them — it routes to both, and asks the browser to keep what it stores.

Frequently Asked Questions

Is OPFS faster than IndexedDB? It depends on the access pattern. For small single-document writes and reads, IndexedDB is faster (about 0.17 ms vs 1.54 ms per write in RxDB's suite) 2. For bulk reads and large binary files, OPFS through a Worker sync handle is roughly two to four times faster 1. Measure your own workload before deciding.

Can I use OPFS on the main thread? Not at full speed. The synchronous createSyncAccessHandle() methods only run inside a dedicated Web Worker — RxDB notes they "cannot be used in the main thread, an iFrame or even a SharedWorker" 1. OPFS has async methods usable on the main thread, but the fast, in-place path requires the Worker, which is where its performance advantage comes from 6.

Which browser storage should a local-first app use? Use IndexedDB as the default for structured, queryable notes and metadata. Use OPFS in a Worker for large blobs and bulk read-heavy work. Use SQLite-WASM on OPFS only when you need real SQL queries 9. Most apps combine them, and call navigator.storage.persist() on top so the data resists eviction 8.

Does OPFS have a bigger storage quota than IndexedDB? No. Both draw from the same per-origin quota. On Chromium browsers an origin can use up to 60% of total disk; Firefox caps best-effort mode at 10% of disk or a 10 GiB group limit 8. Choosing OPFS over IndexedDB does not grant more space — they share one budget.

Can browser storage data be deleted or evicted? Yes. By default both IndexedDB and OPFS are best-effort storage. Under storage pressure the browser evicts the least-recently-used origin's data first 8. To protect it, call navigator.storage.persist(); the eviction policy "skips over origins that have been granted data persistence" 8. Nothing in the browser is permanent by default.

Is IndexedDB really slow, or am I using it wrong? Usually the latter. RxDB calls unbatched IndexedDB "slow … even slower" than a cheap-server database, with a few hundred inserts taking seconds 4. The cost is transaction ceremony and per-row cursor idle time, not the engine. Batch writes into fewer transactions and read in bulk with getAll() 5 and it performs far better.

Can I run SQLite in the browser on OPFS? Yes. SQLite added a WebAssembly build in version 3.40 (November 2022) 9, and its OPFS-backed VFS makes the database persistent. It runs only in a Worker; the classic OPFS VFS also needs SharedArrayBuffer with COOP and COEP headers, while the opfs-sahpool VFS avoids that requirement.

Do OPFS and IndexedDB work in Safari and Firefox? Yes. IndexedDB is universal. The OPFS synchronous access handle reached Baseline "widely available" in March 2023 and sits near 93% global support, having shipped in Chrome and Edge 102 and Firefox 111 10. Safari supports it in its 16.4-era releases. Cross-browser local-first storage is a safe bet in 2026.

Browser storage in 2026 is not a contest with a champion; it is a set of tools with sharp, knowable edges, and the engineer's job is to route each byte to the store that fits it and then ask the browser not to throw it away.


If you want your notes to live on your own device and keep working offline, mnmnote.com is built on exactly that principle.

Related reading on MNMNOTE: Local-first: own your data in 2026 · Plain text when the network fails · The 3-2-1 backup rule for your notes.

Footnotes

  1. "OPFS RxStorage," RxDB, updated 2026-04-23, https://rxdb.info/rx-storage-opfs.html (accessed 2026-07-18). 2 3 4 5 6 7 8

  2. "LocalStorage vs. IndexedDB vs. Cookies vs. OPFS vs. WASM-SQLite," RxDB, updated 2026-06-30, https://rxdb.info/articles/localstorage-indexeddb-cookies-opfs-sqlite-wasm.html (accessed 2026-07-18). 2 3 4 5 6 7 8 9 10

  3. "Origin private file system," MDN Web Docs, https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system (accessed 2026-07-18). 2 3 4 5

  4. "Why IndexedDB is slow and what to use instead," RxDB, https://rxdb.info/slow-indexeddb.html (accessed 2026-07-18). 2 3

  5. Nolan Lawson, "Speeding up IndexedDB reads and writes," 2021-08-22 (2025 update), https://nolanlawson.com/2021/08/22/speeding-up-indexeddb-reads-and-writes/ (accessed 2026-07-18). 2 3

  6. "File System Standard," WHATWG Living Standard, https://fs.spec.whatwg.org/ (accessed 2026-07-18). 2 3

  7. Petr Broz and Michael Beale, "Viewer Performance Update (Part 2/3): OPFS Caching," Autodesk Platform Services, 2024-03-07, https://aps.autodesk.com/blog/viewer-performance-update-part-2-3-opfs-caching (accessed 2026-07-18). 2

  8. "Storage quotas and eviction criteria," MDN Web Docs, https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria (accessed 2026-07-18). 2 3 4 5 6 7 8 9

  9. "SQLite Release 3.40.0 On 2022-11-16," SQLite, https://sqlite.org/releaselog/3_40_0.html (accessed 2026-07-18). 2 3 4

  10. "FileSystemFileHandle: createSyncAccessHandle()," MDN Web Docs, https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/createSyncAccessHandle, and browser support via https://caniuse.com/mdn-api_filesystemfilehandle_createsyncaccesshandle (accessed 2026-07-18). 2