SQLite 放在 Web Worker 裡,不是選配
閱讀時間 7 分鐘 作者 NT²
本機優先保管箱需要裝置上真正的關聯式引擎。若把引擎放在 UI 執行緒, 每一次分頁查詢都會與繪製、輸入搶資源。NT² Vault 在 Web Worker 中執行 SQLite,外層是型別化訊息與單一鎖鏈,讓列表在資料庫保持誠實的同時仍能回應。
SQLite 放在 Web Worker 裡,不是選配
主張很明確:對使用檔案型 SQLite 的瀏覽器保管箱而言,資料庫必須住在 Web Worker——而不是 UI 執行緒。
這不是風格偏好。這是並發邊界:讓本機優先感覺像原生應用,而不是一張一邊「思考」一邊凍結的試算表。NT² Vault 在裝置上加密結構化項目、用 SQL 為列表分頁,並在 Origin Private File System 開啟耐用的資料庫檔。這些操作是實打實的工作。若它們與捲動、輸入、版面配置共用同一執行緒,產品就會用掉幀來支付每一次 commit。
我們不把 Worker 當成 demo 之後才加上的優化。它是儲存架構的一部分。UI 提出請求。Worker 擁有 SQLite。鎖序列化並行呼叫。列表繼續是列表。
限制:同步 SQLite 與可回應的 UI,不能共用一個執行緒
選擇 SQLite 的瀏覽器保管箱,通常想同時擁有三件事:耐用交易、可預期的分頁查詢,以及在負載下仍保持平靜的介面。若引擎跑在響應式與繪製已經存在的地方,這些目標會互相衝突。
WebAssembly 裡的 SQLite 並不便宜。開啟保管箱、遷移 schema、計算篩選列數、抓取一頁、更新全文索引、刷新 commit——都要花費 CPU 與檔案 I/O。在主執行緒上,這些工作與指標事件、鍵盤輸入、CSS layout、Svelte 更新落在同一個事件迴圈。一次「本機」查詢只要幾十毫秒,仍會偷走幀預算。更用力刷新的 commit 偷走更多。使用者體驗到的本機優先,變成卡頓——諷刺的是,正因為裝置正在做他們要求的事。
對我們的儲存路徑,還有第二道更尖銳的限制。
保管箱資料庫是 OPFS 下的真實檔案,透過協作式同步虛擬檔案系統開啟。同步 access handle——以及依賴它們的 VFS——可用於 worker,而不是主執行緒上的快樂路徑。把 SQLite 放在 UI 執行緒,不只會讓介面卡頓;還會對抗我們為了讓 SQLite 像 SQLite 而選擇的檔案系統模型。
即便忽略 OPFS、想像一個非同步 VFS,重疊的 UI 動作仍然危險。兩次點擊、一次搜尋 debounce、一次背景軟刷新,都可能同時想用資料庫。若沒有單一擁有者與序列化佇列,VFS 會看到它從未被設計去吸收的交錯工作。損毀不是唯一風險。沉默的錯誤——部分更新、撕裂讀取、莫名的重新開啟失敗——更糟,因為看起來像「保管箱不穩」,而不是「我們自己在競速」。
因此限制有兩面:
- 回應性: 沉重的 WASM 與檔案 I/O 不可佔用幀迴圈。
- 正確性: SQLite 存取必須被擁有、被排序,而不是灑在每個想查詢的元件裡。
本機優先不是「資料庫跑在 JavaScript 碰巧所在的地方」。它是:裝置擁有真相,而每個執行緒擁有一份不會破壞其他執行緒的工作。
設計:專用 Worker、型別化 RPC、一條鎖鏈
NT² Vault 給 SQLite 一個家:專用 Web Worker 載入同步 WASM 建置,並接到協作式 OPFS VFS。保管箱檔案對每個保管箱有明確路徑。附件密文以獨立檔案放在旁邊。對已解鎖的寫入分頁工作階段而言,Worker 是唯一開啟該 SQLite handle 的地方。
UI 從不把 WASM 模組匯入文件脈絡再「直接 await 一次查詢」。儲存庫與領域程式碼透過型別化訊息協定溝通:開啟、關閉、執行陳述式、抓取分頁、計數,以及產品需要的其他儲存契約。回應以結構化結果跨越邊界。失敗以結構化錯誤跨越邊界。應用層不必知道實體檔是瀏覽器裡的 OPFS,還是桌面上的原生檔——Worker(或其平台等價物)履行同一契約。
在 Worker 內部,存取被序列化。
並行 UI 動作是正常的:虛擬列表快捲到末端時篩選剛好收斂、存一筆項目時分類計數正在刷新、搜尋後不久就鎖定。這些呼叫進入一條 promise 鎖鏈。已開啟交易內的巢狀工作可以加深鎖;並行的頂層呼叫者依序等候。因此 VFS 看到的是有序的資料庫工作,而不是重疊突變的堆疊。序列化不是效能炫耀。它是讓同步 WASM 檔案引擎在同一個分頁裡看不到兩個寫入者的方式。
flowchart LR
UI[SvelteKit UI 執行緒]
RPC[型別化 worker RPC]
Lock[序列化的 SQLite 鎖]
WASM[wa-sqlite 同步 WASM]
VFS[協作式同步 OPFS VFS]
DB[(vault.sqlite)]
UI --> RPC
RPC --> Lock
Lock --> WASM
WASM --> VFS
VFS --> DB
這條管線與技術棧裡另外兩段彼此搭配。
分頁與虛擬化避免 UI 把整個閣樓灌進響應式 state。Worker 可以回答便宜的分頁與計數查詢,同時文件繪製一個滑動視窗。若 UI 仍掛載一萬列,把 SQLite 移出執行緒也救不了;Worker 與列表架構是互補的。
每個保管箱一個寫入分頁避免第二個文件對同一 OPFS 檔開啟競爭 handle。Worker 序列化 寫入分頁內部 的工作。多標籤政策決定 哪個 分頁可以擁有寫入分頁角色。混淆這兩層——讓每個分頁對同一檔案跑自己的 SQLite Worker——只是在更低的高度重現多寫入者問題。
解鎖遵循同一邊界。推導金鑰與驗證本機密碼驗證器是客戶端的事。開啟保管箱資料庫是 Worker 的事。UI 執行緒協調工作階段;它不會在整個解鎖期間變成資料庫執行環境。
取捨:訊息延遲,以及更難的除錯故事
把 SQLite 移進 Worker 並非免費。
每一次查詢都要付訊息往返。在主執行緒上本來只是函式呼叫的小讀取,變成結構化 RPC。工具必須檢查 Worker 日誌,而不只是文件主控台。堆疊追蹤橫跨兩個領域。冷啟動包含 Worker 開機與 WASM 實例化,第一頁才能回來。想「在元件裡直接 db.prepare」的工程師,會覺得架構很固執。
我們接受這項成本。
換回來的是:產品可以成長,而不把解鎖與捲動變成 CPU 競標。分頁查詢與 commit 把時間花在該花的地方。UI 執行緒把時間花在使用者正在看的事:列表動態、表單焦點、複製回饋、鎖定轉換。當保管箱有數千個項目,差異不再是理論。它是本機優先感覺像本機,還是感覺很忙。
還有一項誠實的取捨。我們不假裝 Worker 消掉所有延遲。沉重的 schema 步驟或大型備份遍歷仍然需要牆上時間。真正的勝利是:那些工作執行時不會凍結輸入,而且重疊的 UI 動作不能輕易靠競速破壞底下的 VFS。回應性是讓介面活著、讓資料庫有序——不是宣稱每一條 SQL 陳述式都免費。
有些團隊走中間路徑:為了「簡單」把 SQLite 留在主執行緒,再在查詢周圍灑上 requestIdleCallback 或切塊 await。那只能緩和症狀,修不好擁有權。Idle callback 給不了同步 OPFS handle。切塊也序列化不了兩個寫入者。第一天的便利,會在第一千天變成卡頓與支援噪音。
我們拒絕什麼
把拒絕講清楚,架構也會更清楚。
我們拒絕在 UI 執行緒執行保管箱 SQLite。 在文件脈絡開啟 WASM 模組的 demo,不是結構化保管箱的出貨架構。
我們拒絕對 Worker 資料庫做未序列化的並行存取。 若兩條領域路徑需要該檔案,它們就排隊。巢狀交易深度是刻意的;重疊的頂層寫入不是。
我們拒絕沉默的主執行緒後備路徑。 出貨兩套並發模型——「可能時用 Worker,否則用主執行緒」——會創造兩種效能輪廓與兩種失敗模式。能力檢查屬於 OPFS 與瀏覽器支援邊界,而不是安靜降級、把同步 SQLite 放回幀迴圈。
我們拒絕把 Worker 當成第二個產品。 領域規則、加密與列表分頁留在應用架構。Worker 擁有儲存執行。它不是 UI 端不方便時倒業務邏輯的垃圾桶。
我們拒絕跨分頁的多寫入者 Worker。 一個已解鎖的寫入分頁擁有該保管箱的資料庫連線路徑。跟隨者觀察;他們不對同一檔案開啟競爭的 SQLite 執行環境。
這些拒絕不是反效能調校。它們是本機保管箱在項目數不再是截圖、開始變成人生時,仍然正確且可用的方式。
讓引擎離開幀迴圈
本機優先保管箱仍然需要普通的產品美德:平穩捲動、不解凍整頁的解鎖、不拖曳游標的搜尋,以及不偷下一幀繪製的 commit。這些美德不是靠希望 SQLite 更輕得來的。它們來自:給 SQLite 一個 Worker、給 UI 通往該 Worker 的型別化門戶,並鎖定並行呼叫者,讓底下的檔案系統保持一貫。
這與把保管箱資料庫放在 OPFS 而不是透過 IndexedDB 模擬檔案、為列表分頁而不是把每一列灌進 Svelte state、在標題需要時選擇全文搜尋,以及多個分頁開啟時選出單一寫入分頁,是同一條技術棧故事。儲存位置、查詢形狀與執行緒擁有權是一套架構——不是三個可選升級。
若想了解更廣的本機優先產品形貌,請讀為什麼選擇 PWA 本地優先、零伺服器的 Vault。若想了解 Worker 底下的檔案邊界,見為什麼我們將保管箱的 SQLite 資料庫放在 OPFS,而非 IndexedDB。若想了解查詢回來後 UI 如何保持輕量,見永遠不要把整個保管箱載入 Svelte state以及標題用 FTS5;篩選勝出時走表掃描。若想了解同一檔案周圍的多標籤擁有權,見每個分頁一個已解鎖保管箱——刻意如此。
最後更新 2026-09-16
相關故事
- 為什麼我們將保管箱的 SQLite 資料庫放在 OPFS,而非 IndexedDB
閱讀時間 8 分鐘
- 每個分頁一個已解鎖保管箱——刻意如此
閱讀時間 7 分鐘
- 為什麼選擇 PWA 本地優先、零伺服器的 Vault
閱讀時間 5 分鐘