WASM SQLite 加速 Notion 網頁訪問速度
三年前, 我們成功地 通過使用 SQLite 數據庫在客戶端緩存數據來加速 Mac 和 Windows 上的 Notion 應用程序 [1] 。我們還在原生移動應用程序中使用這種 SQLite 緩存。
今年, 我們能夠爲通過網絡瀏覽器訪問 Notion 的用戶提供同樣的改進。本文深入探討了我們如何使用 sqlite3 的 WebAssembly(WASM) 實現 [2] 來提高 Notion 在瀏覽器中的性能。
使用 SQLite 在所有現代瀏覽器中將頁面導航時間提高了 20%。對於由於外部因素 (如互聯網連接) 而遭受特別慢的 API 響應時間的用戶來說, 這種差異更加明顯。例如, 澳大利亞用戶的頁面導航時間加快了 28%, 中國用戶加快了 31%, 印度用戶加快了 33%。
WASM SQLite 將從一個頁面導航到另一個頁面所花費的時間減少了 20%。
讓我們來看看我們如何在瀏覽器上設置 SQLite!
核心技術: OPFS 和 Web Workers
爲了在會話之間持久化數據, WASM SQLite 庫使用 Origin Private File System (OPFS)[3] , 這是一個現代瀏覽器 API, 允許網站在用戶設備上讀取和寫入文件。
WASM SQLite 庫只能在 Web Workers[4] 中使用 OPFS 作爲其持久層。Web Worker 可以被認爲是在瀏覽器中執行大多數 JavaScript 的主線程之外的單獨線程中運行的代碼。Notion 與 Webpack[5] 打包在一起, 幸運的是它提供了一個 易於使用的語法 [6] 來加載 Web Worker。我們設置了我們的 Web Worker, 要麼使用 OPFS 創建 SQLite 數據庫文件, 要麼加載現有文件。然後我們在這個 Web Worker 上運行我們現有的緩存代碼。我們使用了優秀的 Comlink[7] 庫來輕鬆管理主線程和 Worker 之間的消息傳遞。
我們基於 SharedWorker 的方法
我們的最終架構基於 Roy Hashimoto 在 這個 GitHub 討論 [8] 中提出的新穎解決方案。Hashimoto 描述了一種方法, 其中一次只有一個標籤頁訪問 SQLite, 同時仍然允許其他標籤頁執行 SQLite 查詢。
這種新架構是如何工作的? 簡而言之, 每個標籤頁都有自己專用的可以寫入 SQLite 的 Web Worker。然而, 只有一個標籤頁被允許實際使用其 Web Worker。SharedWorker 負責管理哪個是 "活動標籤頁"。當活動標籤頁關閉時, SharedWorker 知道選擇一個新的活動標籤頁。爲了檢測關閉的標籤頁, 我們在每個標籤頁上打開一個無限開放的 Web Lock - 如果該 Web Lock 關閉, 標籤頁一定已經關閉。
我們的 WASM SQLite 實現的基於 SharedWorker 的架構。
要執行任何 SQLite 查詢, 每個標籤頁的主線程將該查詢發送到 SharedWorker, 後者將其重定向到活動標籤頁的專用 Worker。任意數量的標籤頁可以同時進行任意次數的 SQLite 查詢, 它總是會被路由到單個活動標籤頁。
每個 Web Worker 使用 OPFS SyncAccessHandle Pool VFS[9] 實現訪問 SQLite 數據庫, 這在所有主要瀏覽器上都可以工作。
在接下來的部分中, 我們將解釋爲什麼我們需要以這種方式構建它, 以及當我們嘗試不同方法時遇到了哪些障礙。
爲什麼更簡單的方法不起作用
在構建上述架構之前, 我們嘗試以更直接的方式運行 WASM SQLite - 每個標籤頁一個專用 Web Worker, 每個 Web Worker 寫入 SQLite 數據庫。
我們可以選擇兩種替代的 WASM SQLite 實現:
我們最終發現, 如果以直接的方式使用, 這兩種方法都不足以滿足我們的需求。
絆腳石 #1: 跨源隔離
OPFS 通過 sqlite3_vfs 需要您的網站是 "跨源隔離" 的。爲頁面添加跨源隔離涉及設置一些限制可加載腳本的安全標頭。瞭解更多相關信息的好地方是 "COOP 和 COEP 解釋 [10] "。
設置這些標頭本來是一項重大任務。對於跨源隔離, 僅在您的頁面上設置這兩個標頭是不夠的。您的應用程序加載的所有跨源資源都必須設置不同的標頭, 所有跨源 iframe 都必須附加一個額外的屬性, 允許它們在跨源隔離環境中工作。在 Notion, 我們依賴許多第三方腳本來支持我們網絡基礎設施的各種功能, 實現完全的跨源隔離需要要求每個供應商設置新標頭並更改其 iframe 的工作方式——這是一個不切實際的要求。
在我們的測試中, 我們能夠通過在 Chrome 和 Edge 瀏覽器上使用 Origin Trials[11] 來爲 SharedArrayBuffer 發佈這個變體到一部分用戶, 從而獲得關鍵的性能數據。這些 Origin Trials 允許我們暫時繞過跨源隔離的要求。
使用這種變通方法意味着我們只能在 Chrome 和 Edge 中啓用此功能, 而不能在 Safari 等其他常用瀏覽器中啓用。但來自這些瀏覽器的 Notion 流量足以收集一些性能數據。
爲 Chrome 用戶在您的域上啓用 SharedArrayBuffer(WASM SQLite 的先決條件) 的 Origin Trial, 無需啓用跨源隔離。
障礙 #2: 損壞問題
當我們爲一小部分用戶啓用 OPFS via sqlite3_vfs 時, 我們開始看到一些用戶出現嚴重的錯誤。這些用戶會在頁面上看到錯誤的數據——例如, 將評論歸因於錯誤的同事, 或者鏈接到一個新頁面, 其預覽是一個完全不同的頁面。
顯然, 我們不能在這種狀態下將此功能推廣到 100% 的流量。查看受此錯誤影響的用戶的數據庫文件, 我們注意到一個模式: 他們的 SQLite 數據庫以某種方式損壞。在某些表中選擇行會拋出錯誤, 當我們檢查行本身時, 我們發現數據一致性問題, 如多行具有相同的 ID 但內容不同。
這顯然是不正確數據的原因。但 SQLite 數據庫是如何進入這種狀態的? 我們假設問題是由併發問題引起的。可能打開了多個標籤頁, 每個標籤頁都有一個專用的 Web Worker, 與 SQLite 數據庫保持活躍連接。Notion 應用程序經常寫入緩存——每次從服務器獲取更新時都會這樣做, 這意味着標籤頁會同時寫入同一個文件。儘管我們已經在使用將 SQLite 查詢批處理在一起的事務方法, 但我們強烈懷疑損壞是由於 OPFS API 對併發處理不當造成的。 SQLite 論壇上的一些討論 [12] 似乎證實了其他人也在爲 OPFS 如何管理併發 (或者說, 幾乎不管理) 而苦惱。
我們觀察到損壞問題時 WASM Sqlite 的架構。
因此, 我們開始記錄損壞錯誤, 然後嘗試了一些臨時方法, 如添加 Web Locks[13] 並只讓焦點標籤頁寫入 SQLite。這些調整降低了損壞率, 但還不足以讓我們有信心再次將該功能開啓到生產流量。不過, 我們確認了併發問題顯著地導致了損壞。
Notion 桌面應用沒有遇到這個問題。在該平臺上, 只有一個父進程會寫入 SQLite; 你可以在應用中打開任意多的標籤頁, 但只有一個線程會訪問數據庫文件。我們的移動原生應用一次只能打開一個頁面, 但即使它有多個標籤頁, 在這方面也有類似於桌面應用的架構。
障礙 #3: 替代方案只能在一個標籤頁中運行
我們還評估了 OPFS SyncAccessHandle Pool VFS[8] 變體。這個變體不需要 SharedArrayBuffer,這意味着它可以在 Safari、Firefox 和其他沒有 SharedArrayBuffer 源試用的瀏覽器上使用。
這個變體的權衡是它一次只能在一個標籤頁中運行;任何嘗試在後續標籤頁中打開 SQLite 數據庫的操作都會簡單地拋出錯誤。
一方面,這意味着 OPFS SyncAccessHandle Pool VFS 沒有 OPFS via sqlite3_vfs 變體的併發問題。當我們將其開放給一小部分用戶時,我們確認了這一點,並且沒有看到任何損壞問題。另一方面,我們也不能直接推出這個變體,因爲我們希望所有用戶的標籤頁都能受益於緩存。
解決方案
事實上,這兩個變體都不能開箱即用,這促使我們構建了上述 SharedWorker 架構,該架構與這兩個 SQLite 變體都兼容。當使用 OPFS via sqlite3_vfs 變體時,我們避免了損壞問題,因爲一次只有一個標籤頁進行寫入。當使用 OPFS SyncAccessHandle Pool VFS 變體時,所有標籤頁都可以通過 SharedWorker 進行緩存。
在我們確認該架構在兩個變體上都能正常工作,性能提升在我們的指標中顯而易見,並且沒有損壞問題後,是時候做出最終選擇,決定發佈哪個變體了。我們選擇了 OPFS SyncAccessHandle Pool VFS,因爲它不需要跨源隔離,這本來會阻止我們在 Chrome 和 Edge 之外的任何瀏覽器上推出。
緩解迴歸
當我們首次向用戶推出這項改進時,我們注意到了一些需要修復的迴歸問題,包括加載時間變慢。
頁面加載變慢了
我們的第一個觀察是,雖然從一個 Notion 頁面導航到另一個頁面變快了,但初始頁面加載變慢了。經過一些性能分析,我們意識到頁面加載通常並不受數據獲取的瓶頸限制——我們的應用啓動代碼在等待 API 調用完成時執行其他操作(解析 JS、設置應用等),因此不會像導航那樣從 SQLite 緩存中受益。
爲什麼變慢了?因爲用戶必須下載和處理 WASM SQLite 庫,這阻塞了頁面加載過程,阻止了其他頁面加載操作同時進行。由於這個庫有幾百千字節,額外的時間在我們的指標中是顯而易見的。
爲了解決這個問題,我們對庫的加載方式做了一些修改——我們完全異步加載 WASM SQLite,並確保它不會阻塞頁面加載。這意味着初始頁面數據很少會從 SQLite 加載。這沒問題,因爲我們客觀地確定,從 SQLite 加載初始頁面帶來的加速並不足以抵消下載庫造成的減速。
推出這個變更後,我們的初始頁面加載指標在實驗的測試組和對照組之間變得相同。
慢速設備無法從緩存中受益
我們在指標中注意到的另一個現象是,雖然從一個 Notion 頁面導航到另一個頁面的中位數時間變快了,但 95 百分位時間變慢了。某些設備,如瀏覽器指向 Notion 的移動手機,並沒有從緩存中受益,實際上情況甚至變得更糟。
我們在我們移動團隊之前進行的一項調查中找到了這個謎題的答案。當他們在我們的原生移動應用中實現這種緩存時,一些設備,如較舊的 Android 手機,從磁盤讀取的速度極其緩慢。因此,我們不能假設從磁盤緩存加載數據會比從 API 加載相同的數據更快。
由於這項移動調查的結果,我們的頁面加載已經有了一些邏輯,通過這些邏輯我們將兩個異步請求(SQLite 和 API)進行 "競賽"。我們只是在導航點擊的代碼路徑中重新實現了這個邏輯。這使得我們兩個實驗組之間的導航時間 95 百分位變得相等。
結論
在瀏覽器中爲 Notion 提供 SQLite 的性能改進面臨着一系列挑戰。我們遇到了一些未知問題,特別是圍繞新技術,並在此過程中學到了一些經驗教訓:
-
OPFS 本身並不提供優雅的併發處理。開發者應該意識到這一點並圍繞它進行設計。
-
Web Workers 和 SharedWorkers(以及本文未提及的它們的表親 Service Workers)具有不同的功能,如果需要,將它們結合起來可能會很有用。
-
截至 2024 年春季,在複雜的 Web 應用程序上完全實現跨源隔離並不容易,特別是如果你使用第三方腳本的話。
通過使用 SQLite 在瀏覽器中爲我們的用戶緩存數據,我們觀察到前面提到的導航時間提升了 20%,而且沒有看到其他指標有所退步。重要的是,我們沒有發現任何可歸因於 SQLite 損壞的問題。我們將最終方法的成功和穩定性歸功於官方 SQLite WASM 實現背後的團隊,以及 Roy Hashimoto 和他們公開的實驗性方法。
有興趣爲 Notion 的這類工作做出貢獻嗎?查看我們的 開放職位→[14]
參考鏈接
- 通過使用 SQLite 數據庫在客戶端緩存數據來加速 Mac 和 Windows 上的 Notion 應用程序: https://www.notion.so/blog/faster-page-load-navigation
- sqlite3 的 WebAssembly(WASM) 實現: https://sqlite.org/wasm/doc/tip/about.md
- Origin Private File System (OPFS): https://developer.mozilla.org/en-US/docs/Web/API/FileSystemAPI/Originprivatefilesystem
- Web Workers: https://developer.mozilla.org/en-US/docs/Web/API/WebWorkersAPI/Usingwebworkers
- Webpack: https://webpack.js.org/
- 易於使用的語法: https://webpack.js.org/guides/web-workers/
- Comlink: https://github.com/GoogleChromeLabs/comlink
- 這個 GitHub 討論: https://github.com/rhashimoto/wa-sqlite/discussions/81
- OPFS SyncAccessHandle Pool VFS: https://sqlite.org/wasm/doc/trunk/persistence.md#vfs-opfs-sahpool
- "COOP 和 COEP 解釋: https://docs.google.com/document/d/1zDlfvfTJ9e8Jdc8ehuV4zMEu9ySMCiTGMS9y0GU92k/edit
- Origin Trials: https://developer.chrome.com/docs/web-platform/origin-trials
- SQLite 論壇上的一些討論: https://sqlite.org/forum/forumpost/5543370423fe67d0
- Web Locks: https://developer.mozilla.org/en-US/docs/Web/API/WebLocksAPI
- 開放職位→: https://www.notion.so/careers
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/8mkufMtV9pU_VYb7liNMLA