OPFS sync access handle 讓 SQLite 能在瀏覽器只讀用得到的 page

OPFS(Origin Private File System) 是給網頁用的私有檔案系統,每個 origin 一份,使用者看不到、也不會出現在下載資料夾。它跟 IndexedDB 的根本差別是它真的是檔案:可以 seek 到任意位置、讀某個區間、寫某個區間;IndexedDB 只能整筆存取。

const root = await navigator.storage.getDirectory();
const file = await root.getFileHandle('db.sqlite', { create: true });
const handle = await file.createSyncAccessHandle();
 
handle.read(buffer, { at: 4096 });    // 同步,沒有 await
handle.write(buffer, { at: 8192 });   // 同步

那兩行沒有 await——這是瀏覽器儲存生態裡少數的同步 I/O,也是它存在的唯一理由。代價是只能在 Worker 裡用(主執行緒禁止,同步 I/O 會卡住畫面)。

SQLite 為什麼非它不可

SQLite 的核心是同步的,VFS 介面長這樣:

int xRead(sqlite3_file*, void* buf, int amt, sqlite3_int64 offset);

「從 offset 讀 amt 個 byte 到 buf」——回傳時資料就必須在 buf 裡了,沒有 callback、沒有 promise 的位置。所有非同步儲存 API(IndexedDB、fetch、File API)都接不上這個介面。這就是為什麼過去要在瀏覽器跑 SQLite 只能整個載進記憶體——因為記憶體存取是同步的。

有了它,SQLite 才能做它在 server 上做的事:只讀用得到的 page,300MB 的 DB 常駐可能只剩幾 MB 的 page cache。

代價

官方 sqlite-wasm 的主要 OPFS VFS 需要 SharedArrayBuffer + Atomics.wait,而 SharedArrayBuffer 需要 server 送 COOP/COEP header 開啟 cross-origin isolation——在 Module Federation 這類架構下會連帶影響 host 載入的所有 remote,是跨團隊的改動。(另有不需要這些 header 的 opfs-sahpool VFS,代價是並行與檔案數限制。)

另外兩點:資料實際寫到使用者硬碟,可能有法遵問題;Safari 要 16.4 以上。

為什麼不是直接用 IndexedDB

它做不到的正好是查詢需要的:任意欄位排序要事先為每個欄位建索引、模糊搜尋完全沒有、COUNT(*) WHERE 要自己 cursor 數、沒有 join、沒有運算式索引。等於要在 JS 裡重寫一個查詢引擎。它適合當 SQLite 的後端,不適合取代 SQLite。

相關:瀏覽器內 SQLite:把查詢下推到 Worker 的 WASM heap