Engineering

IndexedDB 取代 localStorage:什麼時候該升級

2026 年 5 月 6 日約 10 分鐘閱讀作者:Hao0321 Studio

localStorage 5MB 限制夠用嗎?對小遊戲存個進度夠,但要存對戰回放、棋譜、大量圖片就不夠。IndexedDB 是瀏覽器提供的「真資料庫」 — 容量到 GB 級、有 index 查詢、支援 Promise(透過 idb 套件)。這篇講從 localStorage 升級的時機與實作。

對比表

項目localStorageIndexedDB
容量5–10 MB50% 可用磁碟(GB 級)
API同步非同步
資料型別只能字串JS 物件、Blob、ArrayBuffer
查詢只能 by key可建 index + range query
Schemaobject stores + version
學習曲線5 分鐘1–2 小時

什麼時候該升級

用 idb 套件簡化

原生 IndexedDB API 用 callback 寫起來很痛。idb(5KB)把它包成 Promise:

import { openDB } from 'idb';

const db = await openDB('hao-games', 1, {
  upgrade(db) {
    const store = db.createObjectStore('replays', { keyPath: 'id' });
    store.createIndex('by-score', 'score');
    store.createIndex('by-date', 'createdAt');
  },
});

// 寫
await db.put('replays', {
  id: 'r-' + Date.now(),
  score: 8240,
  createdAt: Date.now(),
  frames: replayFrames,  // Array of objects
});

// 讀
const r = await db.get('replays', 'r-...');

// 查詢
const top = await db.getAllFromIndex('replays', 'by-score', null, 10);

Schema 版本管理

每次改 schema 升 version。idb 自動跑 upgrade callback:

const db = await openDB('hao-games', 2, {
  upgrade(db, oldVersion) {
    if (oldVersion < 1) {
      db.createObjectStore('replays', { keyPath: 'id' });
    }
    if (oldVersion < 2) {
      const store = db.transaction.objectStore('replays');
      store.createIndex('by-score', 'score');
    }
  },
});

從 localStorage 遷移

async function migrateFromLocalStorage() {
  const raw = localStorage.getItem('DM');
  if (!raw) return;
  try {
    const data = JSON.parse(raw);
    await db.put('saves', { id: 'main', ...data });
    localStorage.removeItem('DM');
  } catch (e) {
    console.warn('Migration failed:', e);
  }
}

Range Query 範例

「查 7 天內 score > 5000 的所有 replay」:

const cutoff = Date.now() - 7 * 24 * 3600_000;
const tx = db.transaction('replays', 'readonly');
const idx = tx.store.index('by-date');
const range = IDBKeyRange.lowerBound(cutoff);

const results = [];
for await (const cursor of idx.iterate(range)) {
  if (cursor.value.score > 5000) results.push(cursor.value);
}

Blob 儲存

使用者畫的圖片、錄的音直接存 Blob:

// 從 Canvas 取 Blob
const blob = await new Promise(r => canvas.toBlob(r, 'image/webp', 0.8));
await db.put('drawings', { id: 'd-' + Date.now(), blob, createdAt: Date.now() });

// 讀回顯示
const d = await db.get('drawings', 'd-xxx');
const url = URL.createObjectURL(d.blob);
img.src = url;

跨頁 / 跨 tab 同步

localStorage 有 storage event。IndexedDB 沒有,但可以用 BroadcastChannel:

const channel = new BroadcastChannel('hao-games');
channel.onmessage = (e) => {
  if (e.data.type === 'replay-added') reloadReplays();
};

// 寫入後 broadcast
await db.put('replays', replay);
channel.postMessage({ type: 'replay-added', id: replay.id });

儲存配額(Storage Estimate)

const { usage, quota } = await navigator.storage.estimate();
console.log(`Used ${(usage/1024/1024).toFixed(1)}MB / ${(quota/1024/1024).toFixed(0)}MB`);

滿了之前就要 cleanup。

持久化(Persistent Storage)

瀏覽器在儲存空間吃緊時會自動清掉「臨時」資料。重要資料申請 persistent:

if (await navigator.storage.persisted()) {
  console.log('已持久化');
} else {
  const granted = await navigator.storage.persist();
  console.log('申請結果:', granted);
}

性能對比

操作localStorageIndexedDB
讀小資料 (1KB)0.1ms2ms
讀中資料 (1MB)5ms10ms
讀大資料 (10MB)無法30ms
查詢 (10000 筆)無法(要遍歷)5ms(有 index)

什麼時候別升級

結語

IndexedDB 不是 localStorage 的替代品 — 是另一個工具。多數小遊戲 localStorage 夠用,但當資料規模或查詢需求超過時,IndexedDB 是優雅的升級路徑。idb 套件讓使用體驗大幅提升,建議直接用而不是寫原生 API。