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(Origin Private File System) provides a browser-internal private storage area, and Web Worker lets you run blocking work off the main thread. 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.
These are the questions this post works through.
- Why
OPFS(Origin Private File System)is 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 (Origin Private File System) 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 a copy of a file the user hands over through a file input or drag and drop — there is no way to write back to the original.
Later the File System Access API arrived, letting you read and write local files with user consent. Alongside the user files you open through a picker, the same spec also brought in OPFS, an origin-private storage area an app uses internally. The two target different things: the File System Access API deals with the user-visible file system, while OPFS is an app-only space the user never sees.
OPFS (Origin Private File System) is a browser-internal file system isolated per origin (website address) — closer to an app-only storage area managed by the browser. 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. Then the synchronous access handle inside a Worker, added later and attached only to OPFS files, made it possible to match the synchronous file I/O model that WASM-based apps expect.
- 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 in the File System API is async, but OPFS files support 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. Unlike the user-visible file system, it can be accessed without security checks or permission prompts, so access is fast, and inside a Dedicated Web Worker it even supports synchronous file access.
3. The Three Concepts
OPFS(Origin Private File System), Web Worker, and FileSystemSyncAccessHandle are concepts from three different angles.
3.1. OPFS (Origin Private File System) — 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 OPFS access 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. Early versions of the spec wrongly defined close, flush, getSize, and truncate as asynchronous and some older browsers implemented them that way, but every browser that supports them today implements them 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. In the default mode (readwrite) the handle holds an exclusive lock — only one handle per file — so close finished handles with close().
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, with no permission prompts | 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 it needs permission requests. (Different purposes: File System Access API = user files, OPFS = browser-internal only)
- async only, without
FileSystemSyncAccessHandle→ the synchronousf.read()pattern cannot be built. - Main thread instead of a Worker → blocking calls freeze the UI.
Sync I/O execution flow diagram
TIP
As the table shows, the three roles never overlap. Drop any one of them and the other two cannot complete synchronous file I/O in the browser.
5. Comparing OPFS Access APIs
| API | Sync? | OPFS only | Read/write | Available context |
|---|---|---|---|---|
getFile() | async | ❌ | Read only | Main thread / Worker |
createWritable() | async | ❌ | Write only | Main thread / Worker |
createSyncAccessHandle() | creation is async, handle ops are sync | ✅ OPFS only | Read + write | Dedicated Worker only |
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 handles where to store, Web Worker handles who executes, and FileSystemSyncAccessHandle handles how to read and write.
TIP
This is why the Pyodide code uses all three together. Losing any one of the three breaks 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