IndexedDBはブラウザ内に大量の構造化データを保存できる、事実上唯一の標準データベースです。一方で、APIが低レベルでトランザクションの扱いに癖があり、保存したデータがブラウザの判断で消えることもあります。この記事では、IndexedDBの仕組みと基本操作のコードを示したうえで、容量上限をChrome・Firefox・Safari別の実数で挙げ、デメリットを重要度順に6つ整理し、localStorage・sessionStorage・Cookieとの違いと使い分けを整理します。数値と仕様はMDNのストレージ上限の解説、W3CのIndexed Database API 3.0、WebKitの公式ブログに基づき、コード例はNode.js 26.5.0とfake-indexeddb 6.2.6で実行して挙動を確かめています。
まとめ:IndexedDBのデメリットと容量・使い分けの要点
- IndexedDBは、ブラウザに組み込まれたトランザクション付きのキー・バリュー型データベースです。データベース・オブジェクトストア・インデックス・トランザクションの4つを押さえれば、素のAPIでも読み書きできます。
- IndexedDBの上限はオリジン単位で、Chrome・Edgeはディスク全体の60%、Firefoxは10%か10GiBの小さい方、Safari(macOS 14・iOS 17以降)は約60%。localStorageとsessionStorageはそれぞれ5MiB、Cookieは名前と値の合計4,096オクテットまでです。
- デメリットの筆頭は「トランザクションの自動コミット」と「スキーマ変更時のタブ間ブロック」です。どちらも書き方を知らないと本番で初めて表面化します。
- 既定のbest-effort保存は、容量逼迫時にオリジン単位で丸ごと消えます。消えては困るデータは
navigator.storage.persist()を要求し、サーバー側にも持つのが原則です。 - IndexedDBは非推奨ではありません。非推奨・削除されたのはWeb SQL Databaseで、IndexedDBは3.0の策定が進み、2025年以降も新しいメソッドが追加されています。
- 認証情報はIndexedDBにもlocalStorageにも置かず、HttpOnly属性付きのCookieに入れます。同じオリジンで動くスクリプトはどちらも読めるためです。
IndexedDBとは:構成要素と基本操作のコード
IndexedDBは、W3Cが策定するIndexed Database APIとしてブラウザに組み込まれた、トランザクション付きのキー・バリュー型データベースです。SQLは使わず、JavaScriptのオブジェクトをそのまま格納します。データはオリジン(スキーム・ホスト・ポートの組)ごとに分かれ、別のサイトからは読めません。登場する部品は次の5つです。
| 部品 | 役割 | RDBでの対応 |
|---|---|---|
| データベース | 名前とバージョン番号を持つ入れ物 | データベース |
| オブジェクトストア | レコードの置き場。キーはkeyPath・自動生成・保存時の引数で指定 | テーブル |
| インデックス | キー以外のプロパティで引くための索引 | インデックス |
| トランザクション | readonly・readwrite・versionchangeの3種 | トランザクション |
| リクエスト(IDBRequest) | 操作1回の結果をsuccess・errorイベントで返す | なし |
素のIndexedDB APIによるDB接続・追加・検索・更新・削除
次のコードは、メモ用のデータベースを作り、追加・キーでの取得・インデックス検索・更新・削除までを1本で通したものです。Node.js 26.5.0とfake-indexeddb 6.2.6で実行し、コメントのとおりの結果になることを確認しました(ブラウザではES modulesのスクリプトとして動きます)。
const req = (r) => new Promise((res, rej) => { r.onsuccess = () => res(r.result); r.onerror = () => rej(r.error); });
const done = (tx) => new Promise((res, rej) => { tx.oncomplete = res; tx.onabort = tx.onerror = () => rej(tx.error); });
const open = indexedDB.open("notes-db", 1);
open.onupgradeneeded = () => {
const store = open.result.createObjectStore("notes", { keyPath: "id", autoIncrement: true });
store.createIndex("byTag", "tag");
};
const db = await req(open);
// 追加(add)
let tx = db.transaction("notes", "readwrite");
let store = tx.objectStore("notes");
const id = await req(store.add({ title: "買い物", tag: "home" }));
store.add({ title: "議事録", tag: "work" });
store.add({ title: "請求書", tag: "work" });
await done(tx);
// キーで1件取得(get)とインデックス検索(getAll)
tx = db.transaction("notes", "readonly");
store = tx.objectStore("notes");
console.log(await req(store.get(id))); // { title: "買い物", tag: "home", id: 1 }
console.log((await req(store.index("byTag").getAll("work"))).map((n) => n.title)); // ["議事録", "請求書"]
// 更新(put)と削除(delete)
tx = db.transaction("notes", "readwrite");
store = tx.objectStore("notes");
store.put({ id, title: "買い物(済)", tag: "home" });
store.delete(2);
console.log(await req(store.count())); // 2
await done(tx);
オブジェクトストアとインデックスを作れるのは onupgradeneeded の中だけで、後から増やすときはバージョン番号を上げます(後述のデメリット2)。get() はキーで1件、index("byTag").getAll("work") はインデックスの値が一致するレコードをまとめて返します。範囲で絞るなら IDBKeyRange.bound()、1件ずつ順に処理するなら openCursor() を使います。
addとputの違いと失敗時の巻き戻し
add() は同じキーのレコードが既にあると ConstraintError で失敗し、put() は上書きします。新規作成と更新を区別したい処理は add()、区別しないなら put() を使います。
見落としやすいのは、失敗したリクエストが1つあると、同じトランザクションで先に発行した書き込みまで取り消される点です。W3Cの仕様は、リクエストのerrorイベントを配信した後の手順を「If event’s canceled flag is false, then run abort a transaction」と定めています。検証では、put({ id: 2 }) の後に重複キーの add() を発行したトランザクションは abort イベントで終わり、id=2のレコードも保存されませんでした。エラーハンドラで event.preventDefault() を呼ぶとトランザクションは中止されず、残りの書き込みがコミットされました。1件の失敗を許容してほかを保存したいときだけ、この呼び出しを入れます。
IndexedDB・localStorage・sessionStorage・Cookieの違い
4つの保存先は、容量よりも「誰が読めるか」「いつ消えるか」「同期か非同期か」で性格が分かれます。まず全体像を表で比べます。
| 項目 | IndexedDB | localStorage | sessionStorage | Cookie |
|---|---|---|---|---|
| 容量の目安 | ディスクの10〜60% | 5MiB | 5MiB | 1個約4KB(名前+値) |
| 保存できる値 | 構造化複製できる値 | 文字列のみ | 文字列のみ | 文字列のみ |
| API | 非同期 | 同期 | 同期 | 同期(document.cookie) |
| サーバーへの自動送信 | なし | なし | なし | リクエストごとに送信 |
| Web Workerから | 使える | 使えない | 使えない | Service WorkerはCookie Store APIで可 |
| 消えるタイミング | 削除まで(追い出しあり) | 削除まで(追い出しあり) | タブを閉じたとき | 有効期限またはブラウザ終了 |
| JavaScriptから読めるか | 読める | 読める | 読める | HttpOnlyなら読めない |
localStorageとsessionStorageはどちらもWeb Storage APIで、違いは区切りの単位です。MDNは、localStorageがオリジン単位で共有されブラウザを閉じても残るのに対し、sessionStorageは「origin and browser tabs」で分けられ、タブを閉じると消えると説明しています。ページの再読み込みや復元では残るため、入力途中のフォームを守る用途に向きます。
IndexedDBが他と決定的に違うのは、オブジェクトをそのまま保存できる点と、Web Workerから使える点です。localStorageの setItem() は同期処理でメインスレッドを止めるので、数MB単位のJSONを頻繁に出し入れすると、端末性能や処理時間によってUIの応答が遅れる場合があります。比較表の4つのうち、大量データの読み書きをWorkerへ逃がせるのはIndexedDBだけです(ファイル単位ならOPFSもWorkerから使え、同期アクセスはWorker専用です)。
ストレージ容量の上限:ブラウザ別の実数
「IndexedDBの容量は何GBか」に単一の答えはありません。上限はブラウザごとの方式で、しかもディスク容量に比例します。MDNの記述を整理すると次のとおりです。
IndexedDBの上限:Chrome・Firefox・Safari
| ブラウザ | 1オリジンの上限 | 1TiBディスクでの例 |
|---|---|---|
| Chrome・Edge | ディスクの60% | 約614GiB |
| Firefox(best-effort) | 10%か10GiBの小さい方 | 10GiB |
| Firefox(persistent) | 50%(最大8TiB) | 512GiB |
| Safari(macOS 14・iOS 17以降) | ディスクの約60% | 約614GiB |
| WKWebViewで表示するアプリ | ディスクの約15% | 約154GiB |
Firefoxの10GiBは、同じサイト(eTLD+1)に属するオリジン全体で共有する「group limit」です。Safariでは、ホーム画面やDockに追加したWebアプリはブラウザと同じ約60%の枠を使えます。また、別オリジンのiframeには親のおよそ10分の1の枠が割り当てられます。埋め込み先でCookieや保存領域がトップレベルのサイトごとに分けられる挙動はiframeのsandbox属性とCookie分割の実装判断で扱っています。ブラウザ全体の天井もあり、MDNによればChromeは全オリジン合計でディスクの80%まで、WebKitはブラウザアプリで80%、Webコンテンツを表示するその他のアプリで20%です。macOS 13・iOS 16以前のSafariは1オリジン1GiBから始まり、超えるとユーザーに許可を求める方式でした。
表の数値は理論上の天井です。MDNは、フィンガープリント対策としてディスク総量から計算しているため「an origin might not actually be able to reach its quota」と注記しています。空き容量が少ない端末では、上限に届く前に書き込みが失敗します。
localStorage・sessionStorageの上限:5MiB
MDNによると、Web Storageは全ブラウザで最大10MiBに制限され、内訳はオリジンごとにlocalStorageが5MiB、sessionStorageが5MiBです。IndexedDBと違ってディスク容量には比例しないため、スマートフォンでもPCでも同じ5MiBで頭打ちになります。
超えた場合、setItem() は例外 QuotaExceededError を投げます。WHATWGのHTML仕様の手順は「If value cannot be stored, then throw a QuotaExceededError.」で、例外を投げてから値を書き込む手順には進まないため、そのキーの値は書き換わりません。
Cookieの上限:1個4,096バイトと個数の制限
Cookieの仕様を改訂し、RFC化の最終段階(RFC Editor待ち)にあるIETFドラフト(draft-ietf-httpbis-rfc6265bis-22)は、名前と値の長さの合計が4,096オクテットを超えるCookieを受け取った場合、そのCookieを丸ごと無視すると定めています。ブラウザが最低限保存すべき数は「At least 50 cookies per domain.」「At least 3000 cookies total.」です。Cookieはリクエストのたびにヘッダーで送られるので、上限まで使うこと自体が通信量の無駄になります。数KBを超えるデータはCookieに入れる対象ではありません。
使用量の確認とQuotaExceededErrorの扱い
自分のオリジンがどれだけ使っているかは navigator.storage.estimate() で取れます。ただし返るのは推定値で、別オリジンから取得したリソースはブラウザが意図的に大きめに数えます。書き込みは必ず例外を捕まえる前提で書きます。
const { usage, quota } = await navigator.storage.estimate();
console.log(`${(usage / 1024 / 1024).toFixed(1)}MiB / ${(quota / 1024 / 1024).toFixed(0)}MiB`);
try {
localStorage.setItem("draft", JSON.stringify(bigObject));
} catch (e) {
if (e.name !== "QuotaExceededError") throw e; // 容量超過以外は握りつぶさない
// 古いキャッシュを消してから再試行する、IndexedDBへ移す等
}
IndexedDBでは例外がその場で投げられず、トランザクションの abort イベントと error に QuotaExceededError が入ります。put() の成功だけを見て保存完了と扱わず、トランザクションの complete を待つ必要があります。
保存期間と消えるタイミング
IndexedDBとlocalStorageに有効期限はありません。それでも「消えないストレージ」ではない点が、デメリットとして最も見落とされます。
MDNは、ブラウザに保存されるデータをbest-effortとpersistentの2種類に分けています。既定はbest-effortで、端末の空きが減る「storage pressure」が起きると、最後に使われた時期が古いオリジンから順に削除されます(LRU方式)。削除はオリジン単位で、MDNは「all of its data, not parts of it, is deleted at the same time」と書いています。IndexedDBだけ、キャッシュだけが消えるのではなく、そのオリジンのIndexedDB・Cache API・OPFSがまとめて消えます。
この追い出しを避ける手段が navigator.storage.persist() です。許可されたオリジンは容量逼迫時の削除対象から外れ、ユーザーが設定から消さない限り残ります。Firefoxではユーザーに許可を求めるポップアップが出ます。ChromeやSafariは確認を出さず、サイトとのやり取りの履歴から自動で許可・拒否を決めるため、false が返る前提で書きます。
if (navigator.storage && navigator.storage.persist) {
const already = await navigator.storage.persisted();
const granted = already || await navigator.storage.persist();
console.log(granted ? "persistent" : "best-effort");
}
Safariにはもう1つ、別系統の削除があります。WebKitは2020年3月24日の公式ブログで、トラッキング防止が有効な場合、ブラウザを使った7日間にそのサイトでのユーザー操作がなければ、スクリプトで書き込んだストレージを削除すると発表しました。対象はIndexedDB、localStorage、sessionStorage、Service Workerの登録とキャッシュなどで、サーバーが Set-Cookie で発行したCookieは対象外です。ホーム画面に追加したWebアプリについては、WebKitの現行のトラッキング防止の解説ページが「The first-party domain of home screen web applications is exempt from ITP’s 7-day cap」と明記しており、そのアプリ自身のドメインのデータはこの7日ルールで削除されません。
プライベートブラウジング(Chromeのシークレットモード等)では、MDNによれば保存APIは使えるものの、データはプライベートウィンドウ(タブ)をすべて閉じた時点で消えます。localStorageもこのモードではsessionStorageと同じ扱いです。Firefoxは115(2023年7月)のリリースノートで、ディスク上の暗号化保存によりIndexedDBが「supported in private browsing without memory limits」になったと告知しており、復号鍵はメモリ上にだけ置かれ、セッション終了時にデータは消去されます。
Cookieだけは有効期限を指定でき、Expires も Max-Age もなければブラウザ終了で消えるセッションCookieになります。ChromeはM104(2022年8月)以降、指定した期限が400日を超えても400日に切り詰めます。Cookieの有効期限とブラウザごとの上限はファーストパーティークッキーの有効期限の上限で詳しく整理しています。
IndexedDBのデメリット6つ
影響の大きい順に並べます。前半3つはコードの書き方で避けられ、後半3つは設計で受け止めるしかない性質です。
1. トランザクションの自動コミット
IndexedDBのトランザクションは、処理待ちのリクエストがなくなりイベントループに制御が戻った時点で自動的にコミットされます。そのため、トランザクションの途中で fetch() や setTimeout など、IndexedDB以外の非同期処理を await すると、戻ってきたときにはトランザクションが閉じています。次のコードをfake-indexeddb 6.2.6で実行すると、2回目の put() は TransactionInactiveError で失敗しました。
const req = (r) => new Promise((res, rej) => { r.onsuccess = () => res(r.result); r.onerror = () => rej(r.error); });
const tx = db.transaction("items", "readwrite");
const store = tx.objectStore("items");
await req(store.put({ id: 1, v: "a" })); // ここまでは成功
await new Promise((r) => setTimeout(r, 0)); // IndexedDB以外の待ち
store.put({ id: 2, v: "b" }); // TransactionInactiveError
サーバーからの取得や重い計算はトランザクションを開く前に済ませ、トランザクション内ではIndexedDBの操作だけを連続して発行します。async/await に慣れたチームほど踏みやすい落とし穴です。
2. スキーマ変更時のタブ間ブロック
オブジェクトストアやインデックスの追加は、indexedDB.open() のバージョン番号を上げ、upgradeneeded イベントの中でしか行えません。このとき古いバージョンで接続したままのタブがあると、アップグレードは待たされます。検証では、古い接続に versionchange イベントが届き、新しい側に blocked イベント(oldVersion=1、newVersion=2)が発火し、古い接続を close() した後にようやくバージョン2へ上がりました。
db.onversionchange = () => {
db.close(); // 古いタブ側で接続を手放す
alert("新しい版があります。再読み込みしてください");
};
この onversionchange ハンドラを書いていないと、デプロイ直後に古いタブを開いたままのユーザーだけ新機能が動かない、という再現しにくい不具合になります。タブ間でどちらが同期処理を担当するかを決める場合は、Web Locks APIによる複数タブの排他制御やBroadcastChannel APIによるタブ間通信と組み合わせます。
3. 低レベルAPIによるコード量の増加
IndexedDBのAPIは IDBRequest に onsuccess と onerror を付けるイベント方式で、Promiseを返しません。1件保存するだけでも、DBを開く、トランザクションを作る、ストアを取る、リクエストの完了を待つ、の4段階を書くことになり、冒頭の基本操作のコードでもPromise化の補助関数を2つ自作しています。イベント処理をPromiseで扱いたい場合は後述のラッパーライブラリが候補です。依存を増やしたくない場合や必要な操作が少ない場合は、素のAPIと数行の補助関数で足ります。
4. 検索機能の弱さと保存できない値
IndexedDBで条件検索に使えるのは、キーとインデックスに対する範囲指定(IDBKeyRange)だけです。SQLのJOIN、部分一致、全文検索はなく、「タイトルに○○を含む」は全件を読み出してJavaScriptで絞り込むことになります。
保存する値は構造化複製(structured clone)で複製されるため、Date・Map・Blob はそのまま入りますが、関数を含むオブジェクトは入りません。検証では、関数を持つオブジェクトの put() が DataCloneError で失敗しました。クラスのインスタンスもメソッドとプロトタイプは失われ、プレーンなオブジェクトとして戻ります。
5. ブラウザ判断によるデータ消失
前章の追い出しとSafariの7日ルールがあるため、IndexedDBを唯一の保存先にすると、ある日ユーザーのデータが丸ごとなくなります。オフライン対応アプリでも、IndexedDBはサーバーの写しを持つキャッシュか送信待ちデータの一時置き場として設計し、正本はサーバーに置きます。
6. 書き込みの耐久性はブラウザ任せ
W3Cの仕様は、トランザクションに durability ヒント("strict"・"relaxed"・"default")を定めています。"strict" はOSのI/Oバッファをフラッシュしてから complete を発火させる想定で、仕様自身が「can take significant time and consume battery life」と書くとおり遅くなります。既定の "default" がどちらで動くかは実装次第で、Chromeは公式ブログで、既定の挙動をChrome 121からstrictからrelaxedへ変えたと告知しています。relaxedは変更がOSのバッファに渡った時点で complete を発火させるため、complete を受け取っても直後の電源断で消える可能性が残ります。電源断やOSクラッシュによる直前の書き込みの消失リスクを下げたい場合は、db.transaction("items", "readwrite", { durability: "strict" }) と明示します。対応はChrome 83、Firefox 126、Safari 15以降です。
「IndexedDBは非推奨」は誤り:現行仕様と削除されたAPI
「indexeddb 非推奨」という検索が一定数ありますが、IndexedDBは非推奨ではありません。W3Cでは2018年1月30日にIndexed Database API 2.0が勧告となり、現在は3.0がWorking Draft(2025年8月13日版)として策定中です。3.0では、キー・主キー・値をまとめて返す getAllRecords() と、getAll() で末尾から取り出す direction 指定が追加されました。ブラウザ互換データ(MDN browser-compat-data)では、getAllRecords() はChrome 141とFirefox 153が対応し、SafariはTechnology Previewのみ、direction 指定はChrome 141だけが対応しています。
実装側の更新も続いています。Chromeは129から、データベースの1ページに収まらない大きな値をSnappyで圧縮して保存するようになりました(Chromeの公式ドキュメントによれば、更新後に保存したデータだけが対象です)。さらにChrome 156のリリースノートには、LevelDBとファイルの組み合わせだったIndexedDBの実装をSQLite上に書き直す移行の第2段階として、新しく作られるデータストアにSQLite版を使うと掲載されています。既存データの移行は第3段階とされ、Web APIの使い方は変わりません。
非推奨どころか削除されたのは、SQLiteをそのまま公開していたWeb SQL Databaseです。Chromeの公式ブログによれば、Chromium 97でサードパーティ文脈から削除、110で非セキュアな文脈から削除され、119ですべての文脈で使えなくなりました。WebKit(Safari)は2019年に削除済みで、Firefoxは最初から実装していません。古い記事で「ブラウザDB=Web SQL」と書かれていても、現在その選択肢はありません。
誤解の背景には、用途によってはIndexedDBより適した保存先が増えたこともあります。Chromeの同じブログは、Web SQLの移行先としてOPFS(Origin Private File System)上で動くWebAssembly版SQLiteを推奨しています。保存領域を用途別に分けるStorage Buckets APIもChrome 122から使えますが、FirefoxとSafariは未対応です。大きなファイルやSQLを使う処理はOPFS、構造化データはIndexedDB、という分担が現実的です。
セキュリティ:XSSで読まれる前提の保存先選定
「IndexedDBは安全か」「localStorageは危険か」への答えは同じです。どちらも同じオリジンで動くJavaScriptからは自由に読み書きでき、アプリ側で暗号化しない限り中身は平文のまま読めます。サイトにXSS脆弱性が1つあれば、攻撃者のスクリプトはIndexedDBの全データもlocalStorageの全キーも読み出せます。sessionStorageもタブ単位になるだけで、同じタブのスクリプトからは読めるので条件は変わりません。
JavaScriptから読めない保存先は、HttpOnly 属性付きのCookieだけです。このため、セッションIDやリフレッシュトークンのように盗まれると乗っ取りに直結する値は、HttpOnly; Secure; SameSite=Lax 以上のCookieに入れ、IndexedDBやlocalStorageには置かないのが原則です。保存先ごとの判断はセッション管理の実装と保存先選定、トークンの寿命設計はリフレッシュトークンの更新フローとローテーション設計、JWTを使う場合の注意はJWTの構造と署名検証の仕組みを参照してください。
Web Crypto APIで暗号化してから保存しても、鍵を同じオリジンに置く限りXSSには効きません。XSS対策の本体は、文脈に応じた出力エンコードやHTMLのサニタイズでスクリプトを注入させないことです。コンテンツセキュリティポリシー(CSP)のnonce方式は、それが破られたときの被害を抑える多層防御として重ねます。
用途別の使い分けとIndexedDBを選ぶべきでない場面
| 用途 | 推奨する保存先 | 理由 |
|---|---|---|
| ログイン状態・トークン | HttpOnly Cookie | JavaScriptから読めない |
| テーマ・言語などの設定 | localStorage | 数KBで同期取得が便利 |
| 入力途中のフォーム | sessionStorage | タブごとに分かれ閉じると消える |
| オフライン用のデータ | IndexedDB | 大容量・Workerから使える |
| 送信待ちの操作キュー | IndexedDB | トランザクションで整合性を保てる |
| 画像・動画・大きなファイル | OPFSまたはIndexedDBのBlob | ファイル単位の読み書き |
| HTTPレスポンスのキャッシュ | Cache API | Service Workerから直接返せる |
IndexedDBを選ぶべきでないのは、保存するデータが数十KBに収まり、読み出しがページ表示時の1回だけというケースです。単純なキー・値の保存ならlocalStorageでコードを短くできますが、同期処理の待ち時間は対象端末で計測して判断します。IndexedDBを入れると、前述のバージョン管理とタブ間ブロックの設計まで背負うことになり、得るものより管理の手間が上回ります。
逆に、1MBを超えるJSONを頻繁に書き換える、Service WorkerやWeb Workerから触る、オフラインで数百件以上のレコードを検索する、のどれかに当てはまればIndexedDBが第一候補です。オフライン対応全体の設計はPWAのService Workerとマニフェストの実装手順で扱っています。スマートフォン向けにオフラインで動くWebアプリを作る場合は、スマートフォンサイト制作の支援もご相談いただけます。
ラッパーライブラリ:idb・Dexie.js・localForage
デメリットの3つ目(APIの低レベルさ)は、ラッパーでほぼ解消できます。npmでの版と直近1週間(2026年10月12〜18日)のダウンロード数は次のとおりです。
| ライブラリ | 最新版 | 週間DL | 性格 |
|---|---|---|---|
| idb | 8.0.4(2026年10月) | 約1,552万 | 素のAPIをPromise化する薄い層 |
| localForage | 1.10.0(2021年8月) | 約467万 | localStorage風のキー値API |
| Dexie.js | 4.4.6(2026年9月) | 約156万 | クエリとスキーマ管理を持つ |
idbは素のAPIの構造をそのまま残してPromiseを返すようにしたもので、IndexedDBの仕組みを理解したまま短く書けます。次のコードはidb 8.0.4で実行を確認したものです。idbと後続のDexieの例は、各パッケージを導入し、バンドラーまたはimport mapでパッケージ名を解決できる環境を前提とします。
import { openDB } from "idb";
const db = await openDB("notes-idb-example", 1, {
upgrade(db) {
const store = db.createObjectStore("notes", { keyPath: "id", autoIncrement: true });
store.createIndex("byUpdated", "updatedAt");
},
});
await db.add("notes", { title: "買い物", updatedAt: Date.now() });
const notes = await db.getAllFromIndex("notes", "byUpdated");
Dexie.jsはスキーマを文字列で宣言し、where() で範囲検索を書けるため、検索が多いアプリで手数が減ります。localForageは最終リリースが2021年8月で更新が止まっているため、新規採用ならidbかDexie.jsを選びます。React環境でライブクエリや楽観的更新まで欲しい場合は、IndexedDBを永続化先に使えるTanStack DBとTanStack Queryの違いも選択肢に入ります。
import Dexie from "dexie";
const db = new Dexie("app-db");
db.version(1).stores({ notes: "++id, updatedAt" });
await db.notes.add({ title: "メモ", updatedAt: 200 });
const recent = await db.notes.where("updatedAt").above(150).toArray();
保存場所の確認と削除の方法
IndexedDBとlocalStorageのデータは、ブラウザのプロファイルフォルダ内に保存されます。中身を確認するには、ChromeとEdgeではDevToolsの[Application]パネル、Firefoxでは[ストレージ]パネル、Safariでは[ストレージ]タブを開き、IndexedDBやLocal Storageの項目からオリジンを選びます。
コードから消す場合は、localStorageなら localStorage.removeItem("キー") または全削除の localStorage.clear()、IndexedDBならデータベースごと消す indexedDB.deleteDatabase("DB名") を使います。deleteDatabase() も、他のタブが接続を開いたままだと blocked で待たされる点はスキーマ変更と同じです。サイト全体のデータをまとめて消したい場合は、サーバーからレスポンスヘッダー Clear-Site-Data: "storage" を返す方法もあります。
よくある質問
IndexedDBの読み方は?
「インデックスド・ディービー」と読みます。Indexed Database APIの略で、名前のとおりキーとインデックス(索引)を使ってレコードを検索します。
ChromeのIndexedDBはどれくらい保存できますか?
1オリジンあたりディスク全体の60%までで、1TiB=1,024GiBの60%は614.4GiBです。ただし実際には空き容量が上限になり、全オリジンの合計がディスクの80%を超えると、best-effortのオリジンから使用が古い順に削除されます。
IndexedDBやlocalStorageのデータはいつ消えますか?
どちらも有効期限はなく、ユーザーがサイトデータを削除するか、コードで消すまで残ります。例外は3つです。容量が逼迫したときのブラウザによる追い出し(navigator.storage.persist() が許可されたオリジンは対象外)、Safariでトラッキング防止が有効なときに、ブラウザを使った7日間にそのサイトで操作がなかった場合の削除、プライベートブラウジングのタブをすべて閉じたときの消去です。
sessionStorageは非推奨ですか?危険ですか?
非推奨ではありません。WHATWGのHTML仕様に定義された現行のAPIです。危険性はlocalStorageと同じで、同じタブ内の同一オリジンのスクリプトから読めるため、そのオリジンにXSSがあれば中身は漏れます。トークンの保存先には使わないでください。
ストレージとキャッシュの違いは何ですか?
ブラウザの文脈では、キャッシュはHTTPレスポンスを再利用するための仕組み(HTTPキャッシュやCache API)で、ストレージはアプリが明示的に書き込むデータの置き場所(IndexedDBやlocalStorage)です。Cache APIはIndexedDBと同じオリジンの保存枠で管理され、容量逼迫時の追い出しではまとめて消えます。HTTPキャッシュはこの保存枠の外でブラウザが管理します。