1. Overview
To do synchronous file I/O in the browser, you have to solve three problems at once: where to store, where to execute, and how to do the I/O. OPFS provides a browser-internal private storage area, Web Worker lets you run blocking work off the main thread, and FileSystemSyncAccessHandle lets you read and write OPFS files synchronously. All three concepts show up together in Pyodide's browser file I/O issue as well.
Here are the key points to check.
- Why
OPFSis needed and how it differs from the existing approaches - How
Web Workerrelates to file I/O - What problem
FileSystemSyncAccessHandlesolves - Why these three concepts always appear together
2. Why OPFS Is Needed
For a long time, file handling in the browser relied on limited APIs based on <input type="file"> or FileReader. The File API is more about reading and processing files that the user provides through a file input or drag and drop.
Later the File System Access API arrived, letting you read and write local files with user consent, but working with the user-visible file system and an origin-private storage area serve different purposes. OPFS is a private storage endpoint per origin — closer to an app-only storage area managed by the browser.
OPFS was created to solve this problem. OPFS (Origin Private File System) is a browser-internal file system isolated per origin (website address). Since it's a storage area never directly exposed to the user, its permission and security flow is simpler than the user-visible file system, and combined with the synchronous access handle inside a Worker, it fits nicely with the file I/O model of WASM-based apps.
- There is a capacity limit — like IndexedDB, it follows the browser storage quota, and you can check usage with
navigator.storage.estimate(). - It gets deleted when site data is cleared.
- The user cannot find the file paths.
- It offers a synchronous API — every other method is async, but OPFS alone supports synchronous access via
FileSystemSyncAccessHandle. The catch: it works only inside a Dedicated Web Worker.
NOTE
OPFS is a browser-internal file system isolated per origin. With no security checks or permission prompts, it can be accessed without async overhead, and inside a Dedicated Web Worker it even supports synchronous file access.
3. The Three Concepts
OPFS, Web Worker, and FileSystemSyncAccessHandle are concepts from three different angles.
3.1. OPFS — Where Do You Store
OPFS is a sandboxed file system managed by the browser. Each origin gets its own independent storage space, invisible in the file explorer. There are two ways in — the async API, available on both the main thread and Workers, and FileSystemSyncAccessHandle, available only inside a Worker.
// async API로 OPFS 접근 (메인 스레드에서도 가능)
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('data.bin', { create: true });
// 쓰기
const writable = await fileHandle.createWritable();
await writable.write('hello');
await writable.close();
// 읽기
const file = await fileHandle.getFile();
const text = await file.text();TIP
OPFS is the "where do you store" question. It is a browser-internal file system isolated per origin, accessed in two ways: the async API (main thread / Worker) and FileSystemSyncAccessHandle (Worker only).
3.2. Web Worker — Who Executes
JavaScript is single-threaded by default. All code runs on the main thread, which also handles UI rendering and event processing, so running heavy computation or blocking I/O on the main thread freezes the screen. A Web Worker creates a separate thread apart from the main thread, and instead of touching the DOM directly, it communicates through postMessage()-based messages.
// 메인 스레드
const worker = new Worker('worker.js');
worker.postMessage({ type: 'start' });
worker.onmessage = (e) => console.log('결과:', e.data);
// worker.js
self.onmessage = async (e) => {
// 여기서 블로킹 작업 수행
self.postMessage({ result: '완료' });
};TIP
The Web Worker is the "who executes" question. To keep the main thread from blocking, synchronous I/O is only allowed inside a Worker.
3.3. FileSystemSyncAccessHandle — How Do You Read and Write
FileSystemSyncAccessHandle is one of the ways to access OPFS files, and as the name says, it reads and writes synchronously (Sync). Methods like read, write, getSize, flush, truncate, and close operate synchronously. The constraints: this handle can only be created on OPFS files, and it is only accessible inside a Dedicated Web Worker.
// Worker 안에서
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('fast.bin', { create: true });
// 핸들 생성은 async (한 번만)
const accessHandle = await fileHandle.createSyncAccessHandle();
// 이 아래는 전부 동기 — await 없음
const encoder = new TextEncoder();
const buf = encoder.encode('Some data');
accessHandle.write(buf, { at: 0 }); // 동기 쓰기
accessHandle.flush(); // 디스크에 반영
const size = accessHandle.getSize(); // 동기 크기 조회
const view = new DataView(new ArrayBuffer(size));
accessHandle.read(view, { at: 0 }); // 동기 읽기
accessHandle.close();Since read()/write() are blocking calls that could freeze the main thread, the spec allows them only inside a Dedicated Web Worker. The createSyncAccessHandle() method itself is an async call that returns a Promise, but the file operation methods on the returned handle are synchronous.
WARNING
Per the spec, FileSystemSyncAccessHandle can only be used inside a Dedicated Web Worker. Calling createSyncAccessHandle() on the main thread throws an error.
4. How the Three Concepts Interlock
Each solves a different problem, but for the goal of "implementing synchronous file I/O in the browser," you need all three at once.
| Problem to solve | Responsible concept |
|---|---|
| Persist files fast and without permissions | OPFS |
| Implement synchronous read / write | FileSystemSyncAccessHandle |
| Keep blocking calls from freezing the UI | Web Worker |
Not one of them can be missing.
- File System Access API instead of OPFS → you can access the user's local files, but security checks make it slow and a permission popup appears every time. (Different purposes: File System Access API = user files, OPFS = browser-internal only)
- async only, without
FileSystemSyncAccessHandle→ the synchronousf.read()pattern is impossible - Main thread instead of a Worker → blocking calls freeze the UI
Sync I/O execution flow diagram
TIP
OPFS is the storage location, the Web Worker is the execution context, and FileSystemSyncAccessHandle is the I/O method. They are different perspectives, each solving a different problem.
5. Comparing OPFS Access APIs
| API | Sync? | OPFS only | Read/write | Recommended context |
|---|---|---|---|---|
getFile() | async | ❌ | Read only | Main thread / Worker |
createWritable() | async | ❌ | Write only | Main thread / Worker |
createSyncAccessHandle() | sync | ✅ OPFS only | Read + write | Worker recommended |
6. Wrapping Up
When I first read about Pyodide's browser file I/O context, the three concepts felt lumped together as one, but separating them by perspective makes everything clear.
- OPFS — where do you store (storage location)
- Web Worker — who executes (execution context)
- FileSystemSyncAccessHandle — how do you read and write (I/O method)
TIP
Each of the three concepts solves its own problem, and together they interlock toward the single goal of "synchronous file I/O in the browser."
7. References
- MDN — File APIdeveloper.mozilla.org
- MDN — Origin private file systemdeveloper.mozilla.org
- MDN — FileSystemSyncAccessHandledeveloper.mozilla.org
- MDN — FileSystemFileHandle: createSyncAccessHandle()developer.mozilla.org
- MDN — Web Workers APIdeveloper.mozilla.org
- MDN — Using Web Workersdeveloper.mozilla.org
- MDN — Introducing workersdeveloper.mozilla.org
- Pyodide Issue #5862 — Sync native FS via FileSystemSyncAccessHandle & OPFSgithub.com