WSS
Web Specification Studio Home
On this page
BlogStoragePerformanceWebAssemblyPublished

OPFS: High-Performance Local File Storage for WASM, SQLite & AI in the Browser

A detailed technical guide to the Origin Private File System (OPFS). Explore synchronous worker APIs, SQLite WASM integration, and how OPFS bypasses IndexedDB serialization overhead.

Reference Card

Spec
File System Standard (WHATWG)
Browser support
Chrome 108+, Safari 15.2+/16.4+, Firefox 111+
Primary use case
High-performance local I/O for SQLite, WASM, and large binary files
Worker requirement
Synchronous API (FileSystemSyncAccessHandle) requires Web Worker
Security requirement
Requires Secure Context (HTTPS)
Persistence
"Best effort" by default; requires navigator.storage.persist() for durability
Last verified
August 2026 against WHATWG Standard and modern browser internals.

Key takeaways

  • ✓ Avoids IndexedDB's structured cloning and transactional object storage model.
  • ✓ Provides byte-oriented synchronous file I/O that minimizes serialization overhead via FileSystemSyncAccessHandle.
  • ✓ Increasingly used by SQLite WASM, FFmpeg.wasm, and client-side ML models.
  • ✓ Exists entirely in a private sandbox; safely separated from the OS file manager.
  • ✓ The high-performance synchronous API is isolated to Dedicated Workers, protecting the main thread from UI jank.

At a glance

Use OPFS if: ✓ SQLite WASM ✓ AI models (LLM weights, embeddings) ✓ Video editing (FFmpeg.wasm) ✓ CAD and complex graphic software ✓ Game assets ✓ Massive binary files

Don’t use OPFS if: ✗ User documents they expect to open in Word/Explorer ✗ Small structured JSON objects ✗ Cross-device syncing

Short answer: The Origin Private File System (OPFS) is a highly optimized, origin-sandboxed virtual file system exposed to the browser. Unlike IndexedDB, which serializes data through structured cloning, OPFS allows WebAssembly and Web Workers to perform raw, synchronous operations directly to the browser-managed storage backend. It is the foundational technology that makes full-blown desktop applications (like SQLite and video editors) viable on the web.

Who this is for: Platform developers building desktop-grade web applications, WebAssembly developers, AI/ML developers porting models to the browser, and anyone pushing the boundaries of client-side performance.

1. The Storage Architecture

To understand OPFS, we must place it in the context of the browser’s storage manager.

Architecture diagram showing Origin Storage Manager branching to IndexedDB, Cache API, and OPFS
Figure 1. The Storage Manager orchestrates different storage APIs. OPFS operates inside a private sandbox entirely separate from the user's OS file manager.

Security and Sandboxing

Security dictates that web pages cannot arbitrarily read a user’s hard drive. OPFS satisfies this by enforcing a strict sandbox:

  • Same-origin policy: https://app.example.com cannot access the OPFS of https://other.example.com.
  • Inaccessible from OS: Not intended to be user-accessible through the operating system’s file manager (like Finder or Explorer). Implementations typically store the data inside browser-managed profile directories.
  • Private browsing: OPFS is available in incognito/private windows, but the entire virtual file system is destroyed the moment the private session ends. Note that availability and quota limits in private mode differ significantly across browser engines (Firefox and Safari are significantly stricter).
  • Secure Contexts: OPFS is only available in Secure Contexts (HTTPS and localhost).
  • Cryptographic Isolation: Note that OPFS is origin-scoped, not cryptographically isolated at the application level. While iOS/Android encrypt the disk at rest, another process attacking the browser profile could extract OPFS data. For truly sensitive data, apply application-level encryption (e.g., Web Crypto API) before writing bytes to OPFS.

2. Browser Support

While OPFS is broadly supported, the asynchronous and synchronous APIs stabilized at different times.

FeatureChrome/EdgeSafariFirefox
Async API (Main Thread)108+15.2+111+
Sync API (Web Worker)108+16.4+111+

Note: Safari (WebKit) and Firefox (Gecko) have historically applied stricter private browsing restrictions and quota heuristics than Chrome (Blink). iOS Safari, in particular, will aggressively evict OPFS data under storage pressure if persistence is not granted.

See the MDN compatibility tables for up-to-date and granular browser support data.

3. The Performance Difference: Why Not IndexedDB?

For over a decade, IndexedDB was the only viable client-side storage mechanism for large data. However, IndexedDB suffers from fundamental architectural bottlenecks that make it unsuitable for high-frequency binary I/O (like running a database engine or video encoder in the browser).

The IndexedDB Bottleneck

Every read/write in IndexedDB requires data to be serialized (copied) crossing boundaries, and executes asynchronously through the browser’s event loop.

The OPFS Advantage

OPFS avoids IndexedDB’s structured cloning and transactional object storage model. By using FileSystemSyncAccessHandle, it grants a Web Worker byte-oriented synchronous file I/O that minimizes serialization overhead.

WASM Note: Some multithreaded WebAssembly applications that use OPFS also rely on SharedArrayBuffer for threading. To use SharedArrayBuffer, your server must emit cross-origin isolation headers (Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp). You can verify this at runtime by checking if (crossOriginIsolated) { ... }.

4. Async vs Sync API: The Web Worker Requirement

The WHATWG File System Standard restricts FileSystemSyncAccessHandle to Dedicated Workers to prevent UI jank on the Main Thread. Attempting to create one on the main thread fails.

Diagram showing the main thread blocked from createSyncAccessHandle, while Web Worker gets direct memory I/O
Figure 2. The main thread is limited to asynchronous streams. To unlock high-performance synchronous I/O, you must offload OPFS operations to a Web Worker.
FeatureAsync APISync API
ContextMain thread, Workers, Service WorkersDedicated Web Worker only
MechanismFileSystemWritableFileStreamFileSystemSyncAccessHandle
Random accessLimitedHigh performance byte-level access
PerformanceGood for bulk downloadsExcellent for high-frequency I/O (Databases, WASM)

Using OPFS on the Main Thread (Asynchronous)

If you are just saving a large file downloaded from a server, or managing directories, the Main Thread async API is sufficient.

// 1. Get the OPFS root directory
const root = await navigator.storage.getDirectory();

// 2. Directory Operations: Create a nested folder
const modelsDir = await root.getDirectoryHandle('models', { create: true });

// 3. Create or open a file inside the new directory
const fileHandle = await modelsDir.getFileHandle('weights.bin', { create: true });

// 4. Create a Writable stream (Asynchronous)
const writable = await fileHandle.createWritable();
await writable.write('Async OPFS!');
await writable.close();

// 5. Directory Iteration
for await (const [name, handle] of modelsDir) {
  console.log(name, handle.kind); // "weights.bin", "file"
}

// 6. Cleanup / Deletion
await modelsDir.removeEntry('weights.bin');

Note: It’s also possible to extract snapshot copies of files using await fileHandle.getFile(), which returns a standard File object.

Using OPFS in a Web Worker (Synchronous)

For SQLite, video encoding, or AI models, you must use a Dedicated Web Worker to access the SyncAccessHandle.

// Inside worker.js
async function writeFast() {
  const root = await navigator.storage.getDirectory();
  const fileHandle = await root.getFileHandle('data.bin', { create: true });

  // 1. Get the synchronous lock (mode defaults to "readwrite")
  // Note: createSyncAccessHandle() is asynchronous, but the methods on the returned handle are synchronous.
  const syncHandle = await fileHandle.createSyncAccessHandle();
  
  // 2. Create a buffer
  const buffer = new TextEncoder().encode('High performance binary data');

  // 3. The Full API Surface:
  syncHandle.write(buffer);            // Write bytes
  syncHandle.flush();                  // Requests that buffered writes be committed to the storage backend
  
  const size = syncHandle.getSize();   // Get file size in bytes
  syncHandle.truncate(size / 2);       // Shrink file
  
  const readBuffer = new Uint8Array(4);
  syncHandle.read(readBuffer, { at: 0 }); // Read specific bytes

  // 4. Release the lock
  syncHandle.close();
}

Sync Handle API Surface

MethodDescription
read()Reads content into a buffer at a specified byte offset.
write()Writes buffer content to the file at a specified byte offset.
flush()Requests that buffered writes be committed to the storage backend.
truncate()Resizes the file to the specified number of bytes.
getSize()Returns the size of the file in bytes.
close()Closes the handle and releases the lock.

5. Lock Management and Lifecycle

By default, opening a SyncAccessHandle in readwrite mode acquires an exclusive lock. While this lock is active, no other process, thread, or worker can read or write to that file.

OperationPurpose
createOpens the SyncAccessHandle and acquires the lock based on the mode.
writeWrites bytes synchronously directly to the browser storage backend.
flushRequests that buffered writes be committed to the storage backend.
closeReleases the lock, allowing other workers or the main thread to access it.
reopenCan only be performed after a successful close().
deleteUses removeEntry() on the parent FileSystemDirectoryHandle. Fails if locked.

Modern browsers support mode configuration during creation:

  • createSyncAccessHandle({ mode: "readwrite" }): Default. Exclusive lock. Attempting to open a second handle in this mode while the first is open will throw a NoModificationAllowedError.
  • createSyncAccessHandle({ mode: "read-only" }): Shared lock. Multiple workers can hold read-only handles simultaneously (await fileHandle.createSyncAccessHandle({ mode: "read-only" })).
  • createSyncAccessHandle({ mode: "readwrite-unsafe" }): Advanced. Multiple handles can write concurrently. The application must orchestrate locking (via Atomics or Web Locks API) to prevent data corruption.
State diagram showing createSyncAccessHandle, lock acquisition, flush, close, and lock release
Figure 3. Failing to call close() on a SyncAccessHandle leaves the file locked, throwing NoModificationAllowedError on subsequent access attempts.

6. Real-World Architecture: SQLite WASM

The most famous success story of OPFS is the official SQLite WASM build. SQLite relies on POSIX C-APIs (open(), read(), write(), fsync()).

To make this work in the browser, SQLite uses its own WebAssembly Virtual File System (VFS) layer. This VFS sits between the core SQLite database engine and the browser, translating C-level POSIX file operations directly into OPFS FileSystemSyncAccessHandle methods.

Diagram showing SQLite translating to WASM VFS, mapping POSIX calls to OPFS SyncAccessHandle
Figure 4. The SQLite WebAssembly Virtual File System translates C-level POSIX file operations directly into OPFS SyncAccessHandle operations.

The result is a full relational database running entirely in the browser, capable of handling gigabytes of data and executing complex SQL queries with high performance for many browser-based workloads.

7. Real-World Architecture: AI Models and FFmpeg

AI/ML (ONNX, GGUF, LLM Weights)

Client-side AI models (like ONNX Runtime Web or Transformers.js) require downloading large tensor weight files (hundreds of megabytes to gigabytes). By storing these in OPFS, the WebAssembly inference engine avoids much of the serialization and transactional overhead associated with IndexedDB, streaming weights directly into memory (or mapping them if memory allows).

Video Editing and Design

FFmpeg expects a native file system to read input frames and write output MP4s. OPFS provides the perfect target for FFmpeg.wasm’s virtual disk. Similar solid local storage patterns are utilized by complex browser-based applications like full-fledged design tools and code editors to manage massive assets.

8. Quotas, Eviction, and Persistence

By default, OPFS data is treated as “Best Effort” storage. If the user’s hard drive gets full, the browser may evict an origin’s best-effort storage according to its storage management policies. Safari on iOS is notoriously aggressive here, clearing best-effort OPFS data if the OS experiences storage pressure.

Diagram showing best effort storage leading to browser eviction, vs persistent storage leading to QuotaExceededError
Figure 5. The Storage Manager eviction policy. Requesting persistent storage protects your OPFS from silent deletion.

Requesting Persistence

If you are building an application where data loss is unacceptable (like an offline code editor), you must ask the browser for persistent storage. You can check the current state using navigator.storage.persisted(). Note that navigator.storage.persist() must be called from the main thread (window context), not from within a Web Worker:

if (navigator.storage && navigator.storage.persist) {
  const isPersistent = await navigator.storage.persist();
  if (isPersistent) {
    console.log("Storage is far less likely to be evicted.");
  }
}

Handling QuotaExceededError

When writing to OPFS, if the disk is physically full or the browser’s origin quota is met, the write() operation will throw a QuotaExceededError. Use navigator.storage.estimate() to proactively check available space before attempting massive downloads:

const estimate = await navigator.storage.estimate();
const availableMB = (estimate.quota - estimate.usage) / (1024 * 1024);
console.log(`Available quota: ${availableMB.toFixed(2)} MB`);

9. OPFS vs File System Access API

It’s easy to confuse the Origin Private File System with the File System Access API.

  • OPFS (navigator.storage.getDirectory()): A private, hidden sandbox. You do not need to prompt the user for permission to read/write here (beyond standard quota limits). The user cannot see these files in their OS file manager.
  • File System Access API (window.showOpenFilePicker()): Prompts the user to select an actual file or folder on their hard drive (e.g., C:\Users\John\Desktop\project). The web app must ask for explicit permission to read or write to these files.

10. Migration and Transfer Strategy

Exporting OPFS to the User’s OS

Since OPFS files are hidden in a sandbox, users cannot double-click them. To let a user save an OPFS file to their Desktop, you must pipe the OPFS read stream into a File System Access API write stream:

// 1. Get user's OS file location via standard save prompt
// Note: showSaveFilePicker() is itself not supported in every browser.
const osFileHandle = await window.showSaveFilePicker({ suggestedName: 'export.sqlite' });
const osWritable = await osFileHandle.createWritable();

// 2. Open OPFS file
const opfsRoot = await navigator.storage.getDirectory();
const opfsFile = await opfsRoot.getFileHandle('internal.sqlite');
const opfsFileObj = await opfsFile.getFile();

// 3. Stream data from sandbox out to the user's Desktop
await opfsFileObj.stream().pipeTo(osWritable);

Should You Migrate from IndexedDB?

Migrate if: You are storing files larger than 10MB (video, audio) or experiencing Main Thread jank due to massive IndexedDB transactions.

Don’t migrate if:

  • You need structured database indexes.
  • You need transactional object storage.
  • You need SQL-like queries (without running SQLite over OPFS).
  • You need automatic cross-tab synchronization.

11. Troubleshooting

ErrorCause
QuotaExceededErrorThe disk is full or the browser’s origin quota has been reached.
NoModificationAllowedErrorYou failed to close() an exclusive SyncAccessHandle and tried to open it again.
NotAllowedErrorThe browser denied access to the file system (often due to permissions or insecure contexts).
TypeErrorAttempted to use the synchronous API on the main thread instead of a Web Worker.
InvalidStateErrorHandle already closed
  • Safari iOS Aggressive Eviction: If your users report missing data on iOS, it is almost certainly quota eviction. You must request persistence.
  • Persistence Denied: navigator.storage.persist() returned false. This is normal. To increase chances of persistence, prompt the user to “Install App” (PWA) or interact with the page before making the request.

12. OPFS API Reference

InterfacePurpose
navigator.storage.getDirectory()Returns the root FileSystemDirectoryHandle for the origin’s OPFS.
FileSystemDirectoryHandleRepresents a directory; used to create/iterate/remove files and folders.
FileSystemFileHandleRepresents a file; used to get File objects or create streams/handles.
FileSystemWritableFileStreamUsed on the main thread for asynchronous writing.
FileSystemSyncAccessHandleUsed in Web Workers for highly performant synchronous byte-oriented I/O.

13. Further Reading and References

14. Browser implementation notes

It is important to remember that browsers implement OPFS differently under the hood:

  • Chromium implements OPFS on top of its own internal storage subsystem (and SQLite in some parts).
  • Different browser engines have entirely different internal file system architectures.
  • Applications should depend strictly on the standard API contract rather than making assumptions about these implementation details, as they are subject to change.

15. Frequently Asked Questions

Does OPFS replace IndexedDB?

No. OPFS is designed for byte-oriented file storage, whereas IndexedDB is a transactional database for structured objects.

Can I view OPFS files on my computer?

No. OPFS files are sandboxed and hidden by the browser. To make a file accessible to the user’s OS file manager, you must stream the data out using the showSaveFilePicker() API.

Why do I get a TypeError when using SyncAccessHandle?

The high-performance synchronous API (FileSystemSyncAccessHandle) is strictly restricted to Dedicated Web Workers. Attempting to use it on the main thread will throw a TypeError.

Will my OPFS data be deleted automatically?

By default, OPFS is considered “best-effort” storage. Under heavy storage pressure, the browser may evict it. To prevent this, you must explicitly request durable storage using navigator.storage.persist().

Can Service Workers access OPFS?

Yes, but only via the asynchronous API. The synchronous FileSystemSyncAccessHandle is restricted strictly to Dedicated Workers.

Does OPFS sync across devices?

No. OPFS is strictly local to the specific device and browser profile where the data was written.

From the team at

We build digital products and explore the modern web standards behind them.