TL

ブラウザストレージとIndexedDBのトランザクションモデル

なぜ書き込みが消えたり競合するのか、その理由がはっきり分かる。IndexedDB のトランザクション分離とオブジェクトストア、容量の退避ポリシー、永続化指定までを内部動作から正確に整理します。

応用ブラウザIndexedDBストレージトランザクションフロントエンド最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.IndexedDB はオブジェクトストアとインデックスからなる非同期キーバリュー DB で、すべての読み書きはトランザクション内でのみ実行できる。
  • 2.トランザクションは readonly / readwrite / versionchange の3モードを持ち、スコープ内のストアにスコープ単位のロックをかけることで分離(直列化に近い保証)を実現する。
  • 3.ストレージは best-effort(容量逼迫時に退避され得る)と persistent(明示許可で退避から保護)に分かれ、退避は origin 単位の LRU で行われる。

IndexedDB は「非同期トランザクショナル KVS」

IndexedDB は、ブラウザ内に構造化データを保存する非同期のキーバリューデータベースです。Web ストレージ が文字列だけの同期 API なのに対し、IndexedDB は任意の構造化複製可能オブジェクト(オブジェクト、配列、ArrayBufferBlobDate など)をそのまま格納でき、すべての操作が非同期かつトランザクション内で行われます。この2点が設計の根幹です。

非同期である理由は、I/O がメインスレッドを止めないためです。localStorage の読み書きは同期でメインスレッドをブロックしますが、IndexedDB の各操作はリクエストオブジェクトを返し、完了は success / error イベント(または Promise ラッパー)で受け取ります。これは イベントループ のタスクとして処理が積まれることを意味します。

データの階層は次のとおりです。1つの origin は複数のデータベースを持て、各データベースは複数のオブジェクトストア(リレーショナル DB のテーブルに相当)を持ちます。各ストアはレコードを主キーで一意に並べ、追加でインデックスを張れます。

概念役割リレーショナル DB での近い概念
データベース名前とバージョンを持つ最上位の入れ物データベース/スキーマ
オブジェクトストア主キー順に並ぶレコード集合テーブル
主キー(keyPath / keyGenerator)レコードを一意に識別し並び順を決める主キー
インデックス別プロパティから主キーを引く副次構造セカンダリインデックス

オブジェクトストアとインデックスの内部

オブジェクトストアは内部的に主キーでソートされた木構造(多くの実装で B-tree 系)として保持されます。これにより、主キーの範囲取得(IDBKeyRange)や openCursor による順次走査が、全件スキャンなしで効率的に行えます。キーには文字列・数値・日付・配列が使え、仕様で定義された全順序で比較されます。

インデックスは「インデックス対象プロパティの値 → 主キー」という対応を保つもう1つのソート済み構造です。インデックス経由の検索は、まずインデックス上で対象キーを二分探索し、得られた主キーでストア本体を引く、という2段の参照になります。インデックスには制約を付けられます。

  • unique:インデックスキーの重複を禁止。違反する書き込みはトランザクションごと失敗(abort)する。
  • multiEntry:対象プロパティが配列のとき、各要素を個別のインデックスエントリにする。タグ検索のような多対多に使う。
インデックスは書き込みコストとして跳ね返る

インデックスは読み取りを速くしますが、put / add のたびに全インデックスの更新が必要です。インデックスが多いストアへの大量書き込みは、その分だけトランザクションが重くなります。書き込みが支配的なワークロードでは、インデックスを必要最小限に絞るか、初期ロードを versionchange 中ではなく専用の一括トランザクションで行うなどの設計が効きます。

トランザクションの3モードとスコープ

IndexedDB のすべての読み書きはトランザクション内でのみ可能で、transaction(storeNames, mode)対象ストア(スコープ)モードを指定して開始します。モードは3種類です。

モード用途並行性
readonly読み取り専用同一ストアに対し複数を並行実行できる
readwrite読み書きスコープが重なる他の readwrite/readonly と直列化される
versionchangeスキーマ変更(ストア/インデックスの作成・削除)そのデータベースを単独で占有する

versionchange は通常の transaction() では作れず、open(name, version) のバージョン番号を上げたときの upgradeneeded イベント内でのみ得られます。ストアやインデックスの作成・削除といったスキーマ変更は、この中でしか行えません。versionchange 中は、その DB に対する他の接続をブロックするため、開いたままの古いタブがあると blocked イベントが発生して昇格が止まります。

分離レベルとロックの粒度

IndexedDB のトランザクション分離は、仕様上直列化(serializable)に近い強い保証を持ちます。これを支えるのがスコープ単位のロックです。トランザクションは開始時に宣言したストア集合に対し、モードに応じたロックを取ります。

ロックの両立性(同一ストアに対して):
  readonly  と readonly   → 両立する(並行読み取り可)
  readwrite と 何か       → 両立しない(書き込みはストアを排他)

スコープが重ならなければ:
  別々のストアを触る readwrite どうし → 並行に走れる

重要なのは、ロックの単位が**レコードではなくストア(テーブル相当)**である点です。あるストアに対する readwrite トランザクションは、そのストア全体を実質的に占有し、スコープが重なる他のトランザクションは前のものが終わるまで待たされます。実装は宣言されたスコープに基づいてトランザクションを順序付け、結果として「同時に走るトランザクションが互いに干渉しない(直列実行と同じ結果になる)」ことを保証します。アプリ側でロックを取る必要はありません。

自動コミットの落とし穴

IndexedDB のトランザクションは明示的な commit を必須としません。イベントループのそのターンで保留中のリクエストが無くなると、トランザクションは自動的にコミットされます。そのため、await fetch() のような IndexedDB 以外の非同期処理をトランザクション中に挟むと、その間にリクエストが空になりトランザクションが先にコミット(実質クローズ)され、後続の putTransactionInactiveError で失敗します。トランザクションの寿命内では、IndexedDB の操作だけを連続させるのが原則です。

書き込み中に制約違反や明示的な abort()、あるいはハンドラ内の例外が起きると、そのトランザクションの全変更がロールバックされます。部分適用は起こらないため、複数レコードの整合した更新を1トランザクションにまとめるのが安全です。

ストレージ量管理と退避(eviction)

ブラウザはオリジンごとに使用量を計測し、デバイスの空き容量に応じたクォータ(多くは全体の数十%を上限とする動的な値)を割り当てます。使用量は navigator.storage.estimate() で概算を取得できます。ここで決定的に重要なのが、ストレージが2つの**永続性モード(persistence)**に分かれる点です。

モード退避(eviction)の対象典型的な扱い
best-effort(既定)なり得る。容量逼迫時に消され得る通常の Web ページの IndexedDB はこちら
persistentならない。ユーザー操作以外で消えない明示的に許可を得たデータに付与される

best-effort のデータは、ディスクが逼迫したときブラウザによって退避され得ます。退避は通常 origin 単位で行われ、各 origin の保存物(IndexedDB だけでなく Cache Storage なども含む)がまとめて消去されます。選定方針は概して **LRU(Least Recently Used)**で、最後にアクセスされてから最も時間が経った origin から消去対象になります。レコード単位で部分的に間引かれるのではなく、origin の領域が丸ごと飛ぶ、というのが退避の実像です。

best-effort のデータは恒久ではない

IndexedDB は「ローカルに残る」だけで「消えない」わけではありません。best-effort のままでは、ユーザーの操作なしにブラウザの判断で消え得ます。サーバーへ再取得できないデータ(オフライン下書き、暗号鍵など)をクライアントだけに置くと、退避で失われる恐れがあります。失っては困るデータは persistent を要求し、かつ可能ならサーバー側にも複製してください。

Persistent Storage の取得

退避から保護したい場合は、navigator.storage.persist()永続化を要求します。これは Promise を返し、許可されれば true、拒否されれば false を返します。

// 現在の永続性モードを確認
const persisted = await navigator.storage.persisted(); // true / false

// 永続化を要求(ユーザー操作や利用実績に応じて許可される)
if (!persisted) {
  const granted = await navigator.storage.persist();
  // granted が true なら、この origin は退避対象から外れる
}

// 使用量とクォータの概算
const { usage, quota } = await navigator.storage.estimate();

許可されるかどうかはブラウザのヒューリスティクスに依存します。サイトがインストール済み PWA である、通知許可がある、エンゲージメントが高い、といった条件で自動的に許可されたり、明示的な許可ダイアログが出たりします(実装差が大きい)。許可されると、その origin のストレージは容量逼迫時の退避対象から外れ、ユーザーが明示的に削除しない限り保持されます。オフライン主体のアプリでは、永続化要求は PWA とサービスワーカー の設計とセットで考えるべき要素です。

なお、永続化は「無限に書ける」という意味ではありません。クォータ上限は依然として存在し、超過時の putQuotaExceededError で失敗します。永続化が保証するのは「勝手に退避されない」ことであって、容量無制限ではない点を取り違えないでください。

まとめ

IndexedDB は、スコープ単位ロックによる直列化に近い分離と自動コミットを備えた、非同期トランザクショナル KVS です。設計の要点は、(1) 1つの整合した更新を1トランザクションにまとめ、その寿命内では IndexedDB 操作だけを連続させる、(2) インデックスは読み取り高速化と書き込みコストのトレードオフで選ぶ、(3) 失っては困るデータは persistent を要求しつつサーバーにも複製する、の3つです。退避ポリシーと永続性モードの理解は、クライアントを「キャッシュ」として正しく扱うための前提になります。機密トークンの置き場所など保存先全般の判断は Cookie とセッション も合わせて整理してください。

Web/フロントエンド Article

ブラウザストレージとIndexedDBのトランザクションモデルを実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

ブラウザ

比較で見る軸

難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5

導入後に効く点

トランザクションは readonly / readwrite / versionchange の3モードを持ち、スコープ内のストアにスコープ単位のロックをかけることで分離(直列化に近い保証)を実現する。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
Web/フロントエンド
タグ数
5

判断チェックリスト

  • 自社の用途が「ブラウザ / IndexedDB」に近いか確認する。
  • 強みである「IndexedDB はオブジェクトストアとインデックスからなる非同期キーバリュー DB で、すべての読み書きはトランザクション内でのみ実行できる。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

ブラウザIndexedDBストレージトランザクションフロントエンドブラウザIndexedDBストレージ
参考: 公式情報