DEV Community

Cover image for OPFS vs IndexedDB
Habibthadev
Habibthadev

Posted on

OPFS vs IndexedDB

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
  });
};
Enter fullscreen mode Exit fullscreen mode

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");
};
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Then create a file:

const file = await root.getFileHandle("data.db", {
  create: true
});
Enter fullscreen mode Exit fullscreen mode

And write to it:

const writable = await file.createWritable();

await writable.write("hello");

await writable.close();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }
  });
Enter fullscreen mode Exit fullscreen mode

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
...
Enter fullscreen mode Exit fullscreen mode

Then:

const PAGE_SIZE = 4096;

function offsetForPage(page) {
  return page * PAGE_SIZE;
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

With OPFS, you put your own layer in the middle:

Application
    |
    v
Database / storage engine
    |
    v
OPFS
    |
    v
Browser storage
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

You can also request persistent storage:

const persistent =
  await navigator.storage.persist();

console.log(persistent);
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
obedsmart profile image
Obed✨✨ • • Edited

This is a great read, great work boss