ブラウザでファイルを同期的に扱う方法

Mar 20, 2026

5 min
💬

ブラウザで同期ファイルI/Oを行うには、保存場所はOPFS、実行コンテキストはWeb Worker、読み書きはFileSystemSyncAccessHandleが担当します。3つの概念がそれぞれどんな問題を解き、なぜいつもセットで登場するのかを、Pyodideのissueを例にたどってみました。

1. 概要

ブラウザで同期的なファイルI/Oを扱うには、保存場所・実行コンテキスト・I/O方式という3つの問題を同時に解く必要があります。OPFS(Origin Private File System) はブラウザ内部専用のストレージを提供し、Web Worker はブロッキング処理をメインスレッドの外で実行できるようにします。FileSystemSyncAccessHandle はOPFSのファイルを同期的に読み書きできるようにします。PyodideのブラウザファイルI/Oのissueでも、この3つの概念が一緒に登場します。

この記事で扱う問いは次のとおりです。

  • OPFS(Origin Private File System) がなぜ必要なのか、従来の方式とどう違うのか
  • Web Worker がファイルI/Oとどんな関係にあるのか
  • FileSystemSyncAccessHandle がどんな問題を解決するのか
  • 3つの概念がなぜいつも一緒に登場するのか

2. なぜOPFS(Origin Private File System)が必要なのか

ブラウザのファイル処理は長い間、<input type="file">FileReader ベースの限定的なAPIに依存してきました。File APIは、ユーザーがfile inputやdrag and dropで渡したファイルのコピーを読み込んで処理する用途に近く、元のファイルに書き戻す手段はありません。

その後 File System Access API が登場し、ユーザーの同意に基づいてローカルファイルを読み書きできるようになりました。このとき、ピッカーで開くユーザーのファイルとは別に、アプリが内部用に使うorigin専用ストレージであるOPFSも同じ仕様に一緒に入ってきました。2つは対象が異なります。File System Access APIが扱うのはユーザーに見えるファイルシステムで、OPFSはユーザーには見えないアプリ専用の領域です。

OPFS(Origin Private File System)は、origin(ウェブサイトのアドレス)単位で分離されたブラウザ内部専用のファイルシステムで、ブラウザが管理するアプリ専用ストレージに近い存在です。ユーザーに直接公開されないストレージなので、ユーザーに見えるファイルシステムより権限・セキュリティのフローがシンプルです。そこに後から追加されたWorker内の同期アクセスハンドルがOPFSファイルだけに付くことで、WASMベースのアプリが期待する同期ファイルI/Oモデルをブラウザで実現できるようになりました。

  • 容量制限があります — IndexedDBと同様にブラウザのストレージクォータに従い、navigator.storage.estimate() で使用量を確認できます。
  • サイトデータを消去すると削除されます。
  • ユーザーはファイルのパスを見つけられません。
  • 同期的なAPIを提供します — File System APIの他のメソッドはすべてasyncですが、OPFSのファイルは FileSystemSyncAccessHandle による同期アクセスをサポートします。ただし、Dedicated Web Worker内でのみ使えます。

NOTE

OPFSはorigin単位で分離されたブラウザ内部専用のファイルシステムです。ユーザーに見えるファイルシステムと違い、セキュリティチェックや権限リクエストなしでアクセスできるため速く、Dedicated Web Worker環境では同期的なファイルアクセスも可能です。

3. 3つの概念

OPFS(Origin Private File System)Web WorkerFileSystemSyncAccessHandle は、それぞれ異なる観点の概念です。

3.1. OPFS(Origin Private File System)— どこに保存するか

OPFSはブラウザが管理するサンドボックス化されたファイルシステムです。originごとに独立したストレージを持ち、ファイルエクスプローラーには表示されません。アクセス方法は2つ — メインスレッド・Workerの両方で使えるasync APIと、Worker内でのみ使えるFileSystemSyncAccessHandleです。

JavaScript
// 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は「どこに保存するか」の問題です。origin単位で分離されたブラウザ内部のファイルシステムであり、アクセス方法はasync API(メイン/Worker)とFileSystemSyncAccessHandle(Worker専用)の2つです。

3.2. Web Worker — 誰が実行するか

JavaScriptは基本的にシングルスレッドです。すべてのコードがメインスレッドで実行され、このスレッドがUIレンダリング・イベント処理も担当するため、重い計算やブロッキングI/Oをメインスレッドで走らせると画面が固まります。Web Worker はメインスレッドから分離された別スレッドを作り、DOMに直接アクセスしない代わりにpostMessage() ベースのメッセージで通信します。

JavaScript
// 메인 스레드
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

Web Workerは「誰が実行するか」の問題です。メインスレッドのブロッキングを防ぐため、OPFSへの同期アクセスはWorker内でのみ許可されています。

3.3. FileSystemSyncAccessHandle — どう読み書きするか

FileSystemSyncAccessHandle はOPFSファイルへのアクセス方法の1つで、名前のとおり同期的(Sync)に読み書きします。readwritegetSizeflushtruncateclose などのメソッドは同期的に動作します。初期の仕様では closeflushgetSizetruncate が誤って非同期として定義され、古い一部のブラウザはそのように実装していましたが、現在サポートしているブラウザはすべて同期的に実装しています。ただし、このハンドルはOPFSファイルに対してのみ作成でき、Dedicated Web Worker内でのみアクセスできます。

JavaScript
// 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();

read()/write() はブロッキングであり、メインスレッドを止めてしまう可能性があるため、仕様上Dedicated Web Worker内でのみ使用できます。createSyncAccessHandle() メソッド自体はPromiseを返す非同期呼び出しですが、返されたハンドルのファイル操作メソッドは同期的です。デフォルトモード(readwrite)ではファイルごとにハンドルを1つしか開けない排他ロックなので、使い終わったハンドルは close() で閉じる必要があります。

WARNING

FileSystemSyncAccessHandle は仕様上、Dedicated Web Worker内でのみ使えます。メインスレッドで createSyncAccessHandle() を呼び出すとエラーになります。

4. 3つの概念が噛み合う構造

それぞれ異なる問題を解決しますが、「ブラウザで同期的なファイルI/Oを実装する」というゴールには3つが同時に必要です。

解決すべき問題担当する概念
権限リクエストなしで速くファイルを永続化OPFS
同期的なread / writeの実装FileSystemSyncAccessHandle
ブロッキング呼び出しがUIを止めないようにWeb Worker

どれか1つでも欠けてはいけません。

  • OPFSの代わりにFile System Access API → ユーザーのローカルファイルへのアクセスはできるものの、セキュリティチェックで遅く、権限リクエストが必要です。(目的が異なる: File System Access API=ユーザーのファイル、OPFS=ブラウザ内部専用)
  • FileSystemSyncAccessHandle なしでasyncのみ → 同期的な f.read() パターンは作れません。
  • Workerなしでメインスレッド → ブロッキング呼び出しがUIを止めてしまいます。

Sync I/O実行フローのダイアグラム

TIP

表からわかるように、3つの役割は重なりません。どれか1つが欠けると、残り2つだけではブラウザでの同期的なファイルI/Oは完成しません。

5. OPFSアクセスAPIの比較

API同期かどうかOPFS専用読み/書き使用可能なコンテキスト
getFile()async読みのみメインスレッド / Worker
createWritable()async書きのみメインスレッド / Worker
createSyncAccessHandle()生成はasync、ハンドル操作はsync✅ OPFSのみ読み + 書きDedicated Worker専用

6. まとめ

最初にPyodideのブラウザファイルI/Oの文脈を読んだときは3つの概念がひとまとまりに感じられましたが、観点ごとに分離すると明確になります。OPFSはどこに保存するかを、Web Workerは誰が実行するかを、FileSystemSyncAccessHandleはどう読み書きするかを、それぞれ担当します。

TIP

Pyodideのコードがこの3つをすべて使う理由はここにあります。3つのうちどれか1つでも欠けると、ブラウザでの同期的なファイルI/Oは成立しません。

7. 参考文献