前端要在瀏覽器存資料,選擇比想像中多。localStorage 最簡單但限制最大,IndexedDB 功能強但 API 難用,sql.js 直接給你完整的 SQL 查詢能力。每個方案都有它適合的場景,選錯了後期很痛苦。
這篇文章從容量、API 型態、查詢能力、效能幾個維度把三者並排比較,最後整理出一個決策流程,讓你在動工前就確定要用哪個。
各方案簡介
localStorage / sessionStorage
最老牌的瀏覽器儲存機制。key-value 字串儲存,API 同步,幾乎不需要學習成本:
| |
sessionStorage 和 localStorage API 完全一樣,差別只在於前者關掉分頁就清空。
限制很明顯:只能存字串、容量上限約 5 MB、沒有任何查詢能力。要找特定資料,你只能自己 JSON.parse 然後在 JavaScript 裡過濾。
IndexedDB
瀏覽器內建的 NoSQL 物件儲存。非同步 API,支援索引(index)和交易(transaction),可以存 JavaScript 物件(包含 Blob、ArrayBuffer),容量彈性大得多。
原生 API 的問題是極度囉嗦。光是開啟資料庫就要寫一堆事件監聽:
| |
實務上幾乎都會搭配 idb 這個封裝函式庫,讓 API 變成 Promise-based。
sql.js(SQLite via Wasm)
sql.js 把 SQLite 編譯成 WebAssembly,讓你在瀏覽器裡跑完整的關聯式資料庫。你可以用標準 SQL 建表、JOIN、GROUP BY、子查詢,全部支援。
| |
最大的限制:資料庫存在記憶體裡,頁面關閉就消失。要持久化,你需要把整個 .db 檔案(Uint8Array)序列化存進 IndexedDB,下次載入時再讀回來。
OPFS + SQLite Wasm(新選項)
Origin Private File System 是較新的瀏覽器 API,提供一個對使用者隱藏的私有檔案系統。官方 SQLite Wasm 支援把資料庫直接存到 OPFS,解決了 sql.js 的持久化問題,不需要靠 IndexedDB 中繼。
瀏覽器支援度(2025 年底)尚可,但 OPFS 的同步 I/O 操作(createSyncAccessHandle)只能在 Web Worker 裡使用,架構複雜度比 sql.js 高一階。
完整比較表格
| 方案 | 容量上限 | API 型態 | 查詢能力 | 持久化 | 適合場景 |
|---|---|---|---|---|---|
| localStorage | ~5 MB | 同步 | 無(需自行過濾) | 持久 | 設定、少量 key-value |
| sessionStorage | ~5 MB | 同步 | 無(需自行過濾) | 分頁關閉即清空 | 暫存表單、頁面狀態 |
| IndexedDB | 磁碟 10–50%(依瀏覽器) | 非同步(event / Promise) | 索引查詢、範圍掃描 | 持久 | 大量物件、二進位資料、離線快取 |
| sql.js | 受限於可用記憶體 | 同步(Wasm 呼叫) | 完整 SQL(JOIN、聚合、子查詢) | 需手動序列化 | 複雜查詢、分析、現有 SQLite 資料 |
| SQLite Wasm + OPFS | 磁碟空間 | 非同步(Worker) | 完整 SQL | 持久(原生) | 需要原生持久化的 SQL 場景 |
容量限制詳解
localStorage:各瀏覽器大多限制在 5 MB,存的是 UTF-16 字元串,超過會丟 QuotaExceededError。
IndexedDB:容量由瀏覽器動態管理。Chrome 最多可用磁碟空間的 80%,但會先進入 "best-effort" 儲存模式,系統磁碟不足時可能被清除。若需保證不被清除,要呼叫 navigator.storage.persist() 申請 "persistent" 模式。
sql.js:整個資料庫存在 ArrayBuffer,上限是 JavaScript 引擎允許的最大記憶體。實測在現代桌機瀏覽器,幾百 MB 的資料庫跑起來沒問題,但行動裝置要小心。
查詢能力比較
用同一個需求說明:找出金額超過 1000 的訂單,並依使用者分組加總。
localStorage 做法
| |
資料量大時,每次查詢都要反序列化整包資料,效能差。
IndexedDB 做法
| |
IndexedDB 的索引讓範圍查詢快很多,但聚合運算還是要在 JavaScript 層完成。
sql.js 做法
| |
查詢邏輯清楚,資料庫引擎負責優化執行計畫。
效能考量
sql.js 初始載入:Wasm 二進位檔約 1 MB,瀏覽器需要下載、編譯並初始化。首次載入通常需要 100–500 ms,之後可以快取。如果你的應用程式本身就很輕量,這個啟動成本會很明顯。
我在實作時會把 initSqlJs() 的 Promise 放在頁面初始化流程裡,和其他資源一起並行載入,避免阻塞使用者操作。
IndexedDB 大量寫入:每個 transaction 有開銷,批量寫入要把資料塞進同一個 transaction,不要一筆一筆開 transaction:
| |
sql.js 大量讀取:因為是記憶體內操作,掃表速度非常快,但要注意把 db.exec() 傳回的 Uint8Array 結果轉成 JS 物件有額外成本,大結果集要做分頁或串流處理。
該怎麼選?
以下是我自己的決策流程:
- 只存設定、token、少量 key-value,且資料 < 5 MB →
localStorage - 需要存物件、二進位檔(圖片、音訊)、或資料量較大 →
IndexedDB - 需要複雜 JOIN、聚合、全文搜尋,且接受頁面關閉後重新載入資料庫 →
sql.js - 需要複雜 SQL 查詢 + 原生持久化,且目標瀏覽器支援 OPFS →
SQLite Wasm + OPFS - 需要 SQL 查詢且資料要跨頁面存活 →
sql.js搭配 IndexedDB 做持久化
組合 sql.js + IndexedDB 是目前相容性最好的「持久化 SQL」方案:
| |
延伸閱讀
這篇是本系列的第三篇,其他兩篇:
- sql.js 入門:在瀏覽器裡跑 SQLite:從安裝到第一個查詢的完整教學
- 用 sql.js + IndexedDB 打造離線 Web App:實際做一個離線可用的應用程式,示範持久化整合
三篇讀完,瀏覽器端資料庫的眉角應該夠清楚了。
