loke.dev

A Direct Line to the Disk

High-performance web apps are outgrowing the limitations of IndexedDB, and the Origin Private File System is the low-latency alternative we've been waiting for.

· 4 min read

IndexedDB is the DMV of web storage: it’s reliable, eventually gets the job done, but the paperwork is exhausting and everything moves slower than you’d like. For years, we’ve tried to force IndexedDB to behave like a high-performance database, but it was never designed for heavy-duty I/O or the raw speed required by modern WASM-powered applications.

If you’ve ever tried to run a full instance of SQLite on top of IndexedDB, you’ve likely felt the pain of the "wrapper tax." Every read and write has to hop through layers of abstraction, transaction locks, and serialization.

The Origin Private File System (OPFS) changes the game by giving us a direct, sandboxed line to the disk. It’s not just another storage API; it’s a dedicated filesystem for your origin that can bypass the main thread’s overhead.

Why move away from IndexedDB?

IndexedDB is great for storing JSON objects or small blobs of user state. But the moment you start dealing with megabytes of binary data—like video editing in the browser, Photoshop-style layering, or local-first databases—the cracks start to show.

OPFS provides a hierarchical directory structure that feels like a real filesystem. More importantly, it offers a Synchronous Access Handle. This is the "secret sauce." By moving file operations to a Web Worker, you can perform synchronous reads and writes. This sounds counter-intuitive (isn't async always better?), but for low-level engines like SQLite, synchronous access to a file descriptor is what makes the difference between "laggy" and "native-speed."

Getting your hands on the root

Accessing the OPFS is surprisingly simple. You don't need a "File Picker" or user permission prompts because this storage is "private" to your origin. The user can't see these files in their Downloads folder, and other websites can't touch them.

async function getRoot() {
  // This is your entry point to the private sandbox
  const root = await navigator.storage.getDirectory();
  return root;
}

// Creating a file is just as easy
async function createMyFile() {
  const root = await getRoot();
  const fileHandle = await root.getFileHandle('app.db', { create: true });
  return fileHandle;
}

The High-Performance Secret: Web Workers

To get the real speed, you have to leave the main thread. In a Web Worker, you can request a FileSystemSyncAccessHandle. This handle allows for read() and write() operations that block the worker thread but are incredibly fast.

Here’s how you’d set that up inside a worker:

// inside-worker.js
self.onmessage = async (e) => {
  const root = await navigator.storage.getDirectory();
  const fileHandle = await root.getFileHandle('data.bin', { create: true });
  
  // This is where the magic happens
  const accessHandle = await fileHandle.createSyncAccessHandle();
  
  const content = new TextEncoder().encode("Hello from the disk!");
  
  // Synchronous write! No 'await' needed here.
  const writeSize = accessHandle.write(content, { at: 0 });
  
  // Always flush to ensure the bits actually hit the platter (or flash)
  accessHandle.flush();
  
  // Close it when you're done to release the lock
  accessHandle.close();
  
  self.postMessage(`Wrote ${writeSize} bytes to OPFS`);
};

Why this matters for SQLite

If you’ve been following the progress of SQLite WASM, OPFS is the reason it’s finally viable for production.

Historically, SQLite in the browser had to "fake" a filesystem by storing the entire database in memory or hackily syncing chunks to IndexedDB. With the OPFS VFS (Virtual File System) for SQLite, the database engine talks directly to the sync access handle. I’ve seen performance boosts of 10x or more compared to the old IndexedDB backends. It makes "Local-First" development feel like a legitimate choice rather than a series of compromises.

The "Gotchas" and Realities

It’s not all sunshine and zero-latency reads. There are a few things that might trip you up:

1. Exclusive Locking: When you open a FileSystemSyncAccessHandle, it locks the file. You cannot open a second handle from another tab or another worker until the first one is closed. This is a classic filesystem lock—great for integrity, annoying for debugging multiple tabs.
2. The Main Thread Limit: You cannot use synchronous access handles on the main thread. If you try, the browser will throw an error to prevent you from freezing the UI.
3. Storage Quotas: While OPFS is more performant, it still lives under the browser's storage quota. If the user’s disk is full or they clear their "Site Data," your OPFS files will go poof just like IndexedDB data would.
4. Invisible to Users: Remember, this is *private*. If your app saves a "Project.zip" here, the user can't find it to email it to a friend unless you specifically write code to export it using the standard File System Access API.

When should you use it?

Don't go rewriting your simple "To-Do List" app to use OPFS today. If you’re just storing a few preferences or a small list of items, IndexedDB (or even a wrapper like Dexie) is perfectly fine.

However, if you are building:
* An IDE or heavy code editor
* A graphics or video processing tool
* A local database-heavy application
* Anything using WASM that expects a C-style filesystem

...then OPFS is your new best friend. It bridges the gap between the web and the metal, making the browser feel less like a sandbox and more like a real operating system. It’s about time we stopped treating the disk like a slow, scary stranger and started treating it like the high-speed resource it is.