Web ストレージ選定フロー:localStorage / sessionStorage / IndexedDB / Cookie / Cache API の使い分け
「ユーザーの操作履歴を端末に保存したい」「ログイン状態を保持したい」「画像をオフラインで開けるようにしたい」——いずれもブラウザのストレージ機能を使う場面ですが、選択肢は 5 つあり、それぞれ向き不向きが大きく違います。本記事では特性を比較表に整理し、典型的な要件に対する選定フローを提示します。
5 つのストレージの比較
| ストレージ | 容量目安 | 寿命 | API | サーバへ送信 | SSR で読める |
|---|---|---|---|---|---|
| localStorage | 5〜10 MB | 永続(手動削除まで) | 同期 | しない | しない |
| sessionStorage | 5〜10 MB | タブを閉じるまで | 同期 | しない | しない |
| IndexedDB | 数百 MB〜GB | 永続 | 非同期 | しない | しない |
| Cookie | 4 KB / 1 個 | Expires で指定 | 同期 | 常に送信 | 読める |
| Cache API | 数百 MB〜GB | 永続(明示削除まで) | 非同期 | しない | しない |
「サーバに送るか」「容量はどれくらい必要か」「どの寿命にしたいか」「サーバ側でも読みたいか」が主な分岐点です。
localStorage:軽量な永続データ向け
最もよく使われる。設定値・ダーク/ライトの切替・「最後に開いたタブ」のような画面状態に向く。
- 容量は 5〜10 MB(ブラウザ実装次第、Chrome は 10 MB)
- 同期 API なので大量書き込みでメインスレッドが詰まる
- 数値・配列・オブジェクトはすべて文字列にシリアライズされる(
JSON.stringify) - ドメイン単位で分離(origin スコープ)
localStorage の罠
- JSON シリアライズ前提なので、
DateやMapがそのままでは保存できない。new Date(JSON.parse(...))のような復元ロジックが必要 - 同期 API のため、5MB ギリギリのデータを書くとフリーズが発生(特にモバイル)
- Safari の Private Browsing ではエラーを投げる(書き込み試行で
QuotaExceededError)。try-catch を必ず
sessionStorage:タブ単位の一時状態
API は localStorage と同じだが、タブを閉じると消えるところだけ違う。
- フォームの未保存入力を一時保存(タブ復帰時に戻す)
- ウィザード型 UI のステップ間 state
- ページ遷移を跨いだ「直前の検索クエリ」
タブを開き直されると消えるので、永続化不要なデータの自動退避場所として使うのが正解。
IndexedDB:大量・構造化データ向け
数百 MB〜GB の大容量データ、画像・PDF・JSON 配列の大量保存に向く。
- 非同期 API(callback / Promise)
- インデックス・トランザクション・カーソル走査ありの本格的な KV ストア
- 標準 API は癖があり、Dexie や idb のような薄いラッパーを使うのが現実解
- Workbox 経由で Service Worker と組み合わせる pattern が多い
IndexedDB が向く例
- オフラインで読めるドキュメント / メール / メッセージのキャッシュ
- ユーザーがアップロードした画像のローカル保管
- 大量の検索インデックス(タイムライン全件など)
IndexedDB の罠
- API が低レベルで複雑。素で書くと
onupgradeneededtransactionのミスでデータが入らない事故が起きる - ブラウザによっては明示的にユーザーが許可しないと長期間保存されない(Safari の eviction policy)
- 開発中、devtools の Storage タブから手動でクリアできる(テスト用に便利)
Cookie:サーバとの状態共有
唯一、HTTP リクエストに自動で付与されるストレージ。
- セッションID、CSRF トークン、認証関連のフラグ
- 1 個あたり 4 KB、合計 50〜180 個(ブラウザ依存)
HttpOnlyを付ければ JS から読めない(XSS 耐性)Secureを付ければ HTTPS のみSameSiteで CSRF 耐性
Cookie が必要なケース
- セッション認証(サーバ側でセッション ID を識別)
- CSRF トークンの埋め込み
- A/B テストの実験割当(サーバ側でも判定したい)
Cookie の使いどころでない例
- 大きなデータ(4KB 制限):localStorage か IndexedDB へ
- JS でしか読まないデータ:Cookie はリクエストごとにオーバーヘッドになる
- 別ドメイン間の共有:3rd-party Cookie のサポートが各ブラウザで縮小中(Chrome は 2024 年に廃止計画を撤回したが、Safari / Firefox では既にデフォルト無効)
Cache API:HTTP レスポンスのキャッシュ
Service Worker と組み合わせて、ネットワークレスポンスをそのまま保存。
cache.put(request, response)でリクエスト/レスポンスのペアを保存- オフライン PWA での画像・JS・CSS のキャッシュに最適
- Request / Response オブジェクトをそのまま扱える(JSON にしなくていい)
- Workbox がこのレイヤを抽象化してくれる
Cache API が向く例
- アプリ shell(HTML / CSS / JS)のオフライン化
- 一覧で表示するサムネイル画像のキャッシュ
- API レスポンスの ETag / 304 を意識した stale-while-revalidate
JSON データそのものを保存したいなら IndexedDB のほうが向きます(インデックスや更新が楽)。Cache API は HTTP の単位で保存・取り出しすると考えるのが正解。
選定フロー
新しい用途で「どこに保存するか」を決めるとき、以下の順で絞れます。
サーバに毎リクエスト送りたい?
- YES → Cookie(サイズ制限と CSRF 対策に注意)
- NO → 次へ
HTTP レスポンスをそのまま保存したい?(PWA / オフライン対応)
- YES → Cache API + Service Worker
- NO → 次へ
データサイズが MB 級以上 or 構造化されている?
- YES → IndexedDB(Dexie 推奨)
- NO → 次へ
タブを閉じても残したい?
- YES → localStorage
- NO → sessionStorage
ユースケース別早見表
- テーマ(ダーク/ライト)切替 → localStorage
- 入力中フォームの一時退避 → sessionStorage
- オフライン対応のドキュメント → IndexedDB(+ Service Worker)
- PWA の app shell → Cache API(+ Service Worker)
- ログインセッション → Cookie(HttpOnly + Secure + SameSite)
- CSRF トークン → Cookie + meta タグの組み合わせ
- 検索履歴 → localStorage(軽い) or IndexedDB(多量)
- 画像のオフラインビューア → IndexedDB(バイナリ) or Cache API(HTTP 単位)
容量とライフサイクルの罠
ブラウザは予告なくストレージを削除することがあります:
- Safari:7 日間サイトに訪問がないと localStorage / IndexedDB / Cache API を削除(Storage Access API 関連の制限)
- Chrome / Firefox:ディスク容量逼迫時に LRU で削除
- eviction の対象になりにくくするには
navigator.storage.persist()でユーザーに許可を求める
「localStorage に保存したから永続」は楽観的すぎる。消える可能性を前提に、サーバとの同期 / 復旧経路を用意するのが安全です。
まとめ
5 つのストレージは「容量 × 寿命 × アクセスパターン」の異なるトレードオフを持ちます。Cookie は唯一サーバと連動する特殊な存在、IndexedDB と Cache API は大容量で非同期、localStorage と sessionStorage は気軽だが容量と性能に上限があります。サーバに送るか送らないかを最初に決めると選択肢が一気に絞れます。
実データのサイズ感を確認したいときは、バイト変換ツール で KB / MB の単位を実数で確認できます。