When you need to store data in the browser, IndexedDB is usually the first API that comes to mind. It's a browser database with object stores, indexes, and transactions, so you don't have to build any of that yourself.
Then there is OPFS, the Origin Private File System. It's much lower level: a private filesystem where your application can create directories and files, which at first makes it look less useful than IndexedDB for application data. I think it gets more interesting once you stop treating it as a database and start treating it as a storage layer.
IndexedDB
IndexedDB is designed around structured data. A simple example:
const request = indexedDB.open("my-app", 1);
request.onupgradeneeded = () => {
const db = request.result;
db.createObjectStore("users", {
keyPath: "id"
});
};
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction("users", "readwrite");
const users = tx.objectStore("users");
users.add({
id: 1,
name: "Habib",
age: 23
});
};
Indexes are created in the upgrade handler too:
const request = indexedDB.open("my-app", 1);
request.onupgradeneeded = () => {
const db = request.result;
const users = db.createObjectStore("users", {
keyPath: "id"
});
users.createIndex("age", "age");
};
For normal applications this is convenient. You store objects and query them through keys and indexes without worrying about how the data is physically stored. For something like an offline notes app, IndexedDB is probably all you need.
OPFS
OPFS takes a different approach. You start with a directory:
const root = await navigator.storage.getDirectory();
Then create a file:
const file = await root.getFileHandle("data.db", {
create: true
});
And write to it:
const writable = await file.createWritable();
await writable.write("hello");
await writable.close();
There is no object store or secondary index here. You have a file, and what goes inside it is up to you.
OPFS as a database storage layer
Imagine building a small database laid out like this:
mydb/
├── data.db
├── wal
├── metadata
└── indexes/
├── users.idx
└── posts.idx
Your database API could look like:
const db = await openDatabase("my-app");
await db.collection("users").insert({
name: "Habib",
age: 23
});
const users = await db
.collection("users")
.find({
age: { $gt: 20 }
});
The application sees a document database. Underneath, you could implement pages, indexes, a write-ahead log, transactions, caching, and whatever storage format you want. OPFS is just where those files live.
A page based storage engine, for example, could divide the database file into 4 KB pages:
0 - 4095 Page 0
4096 - 8191 Page 1
8192 - 12287 Page 2
12288 - 16383 Page 3
...
Then:
const PAGE_SIZE = 4096;
function offsetForPage(page) {
return page * PAGE_SIZE;
}
The engine can then read and write individual pages instead of loading the entire database into memory.
Sync access handles
OPFS also has FileSystemSyncAccessHandle, which is available from dedicated Web Workers.
const root = await navigator.storage.getDirectory();
const file = await root.getFileHandle("data.db", {
create: true
});
const access = await file.createSyncAccessHandle();
With it you can do random access reads and writes:
const page = new Uint8Array(4096);
access.write(page, {
at: 0
});
access.read(page, {
at: 0
});
access.flush();
access.close();
This suits database engines because they already tend to work with pages and offsets. SQLite is a good example of software that benefits from this model:
JavaScript
|
v
SQLite / database engine
|
v
OPFS
The database engine handles the database logic, and OPFS provides the persistent storage.
So which one should you use?
For most web applications, I'd still pick IndexedDB. If I'm storing user preferences, offline forms, cached API data, notes, or small application state, it gives me a useful abstraction immediately.
I'd seriously consider OPFS if I were building a database engine, a local-first database, a browser IDE, a WASM database, or a file-based application, or if I had a large local dataset.
With IndexedDB, the stack looks like this:
Application
|
v
IndexedDB
|
v
Browser storage
With OPFS, you put your own layer in the middle:
Application
|
v
Database / storage engine
|
v
OPFS
|
v
Browser storage
IndexedDB gives you the database abstraction. With OPFS, you build your own.
What about large datasets?
OPFS can be useful when the data is much larger than what you want to keep in memory. A database could be organized like this:
RAM
|
+-- buffer pool
+-- index cache
+-- query state
|
v
OPFS
|
+-- data files
+-- indexes
+-- WAL
You might have gigabytes of data on disk while keeping only a relatively small working set in memory.
There is an important caveat: OPFS is still browser storage. Storage quotas depend on the browser, device, and origin. Data can also be removed when storage is cleared, and best-effort storage can be evicted under storage pressure.
You can check the current estimate with:
const { usage, quota } =
await navigator.storage.estimate();
console.log({
usage,
quota
});
You can also request persistent storage:
const persistent =
await navigator.storage.persist();
console.log(persistent);
Whether the browser grants persistence depends on its storage policy.
OPFS isn't a replacement for IndexedDB
I don't think "which one is better?" is the useful question, because they sit at different levels. IndexedDB gives you a database abstraction, and OPFS gives you a persistent filesystem abstraction.
If I just need to persist application objects, IndexedDB saves me a lot of work. If I want to control how my database stores pages, indexes, logs, and records, OPFS gives me much more room to experiment. The browser is giving us a storage primitive low level enough to build on, which means you could build a database in the browser instead of only using the one it already provides.
Top comments (1)
This is a great read, great work boss