ブラウザにおけるデータ保存の選定基準とベストプラクティス
Webアプリケーションの高度化に伴い、クライアントサイド(ブラウザ)でのデータ管理は避けて通れない課題となっている。サーバーサイドでの管理が基本ではあるが、UXの向上やオフライン対応、サーバー負荷の軽減において、ブラウザストレージの活用は不可欠だ。
本稿では、ブラウザにおけるデータ保存の目的、各ストレージ技術の特徴と使い分け、そして運用上のベストプラクティスについて解説する。
1. なぜブラウザで保存する必要があるのか
HTTPのステートレス性
Webの基盤であるHTTPプロトコルは、基本的にステートレス(状態を持たない)である。サーバーはデフォルトで「直前のリクエスト」と「今回のリクエスト」の関連性を記憶しない。
保存しない場合に生じる問題
クライアント側で状態を保持できない場合、以下のような弊害が発生する。
- 認証の欠如: ページ遷移のたびにログイン情報の再入力が必要になる。
- UXの低下: フォームの入力内容、スクロール位置、UI設定(ダークモード等)がリロードのたびに初期化される。
- パフォーマンス悪化: 静的リソースやマスタデータを毎回サーバーから取得するため、レイテンシが増加する。
これらを解決し、**「状態の維持」「パフォーマンス向上」「オフライン対応」**を実現するためにクライアントサイドストレージが利用される。
2. 保存手段の特徴とユースケース
現在、モダンブラウザで標準的に利用可能な保存手段は主に5つある。
① Cookies(クッキー)
最も古くから存在する保存領域。
特徴:
- リクエスト時にHTTPヘッダーに自動付与され、サーバーへ送信される。
- 容量が非常に小さい(4KB程度)。
- 有効期限(Expires/Max-Age)の設定が可能。
ユースケース:
- 認証トークン(Session ID): HttpOnly属性を付与することでJavaScriptからのアクセスを防げるため、最もセキュア。
- サーバー側と共有が必要なトラッキングIDや設定値。
② LocalStorage(ローカルストレージ)
Web Storage APIの一種。シンプルなKVS(Key-Value Store)。
特徴:
- ブラウザを閉じてもデータが永続的に残る。
- APIが同期処理であり、大量データの読み書きはメインスレッドをブロックする可能性がある。
- 容量はオリジンごとに5MB〜10MB程度。
ユースケース:
- 永続的なUI設定(テーマ、言語設定)。
- 簡易的なユーザー設定。
- 注意: 頻繁なI/Oが発生するものや、機密情報の保存には向かない。
③ SessionStorage(セッションストレージ)
LocalStorageと同様のAPIだが、データの寿命が異なる。
特徴:
- タブ(ウィンドウ)を閉じるとデータが破棄される。
- 別タブで同じサイトを開いてもデータは共有されない。
ユースケース:
- 入力中のフォームデータの一時保存(リロード対策)。
- 特定のセッション内でのみ有効なフィルタリング条件やページネーション位置。
④ IndexedDB
ブラウザ内蔵のNoSQLデータベース。
特徴:
- 大容量データの保存が可能(ディスク空き容量に依存)。
- 非同期I/Oのため、メインスレッドをブロックしない。
- インデックスによる高速な検索、トランザクション管理が可能。
- 生のAPIは複雑であるため、実務では Dexie.js などのラッパーライブラリを用いるのが一般的。
ユースケース:
- オフラインアプリケーション(PWA)のデータストア。
- 画像やファイル(Blob)の保存。
- 数万件規模のマスターデータキャッシュ。
⑤ Cache API
Service Workerと連携して使用されるネットワークリクエスト専用ストレージ。
特徴:
- リクエスト(URL)とレスポンス(HTML/CSS/JS/画像)のペアを保存する。
ユースケース:
- アプリケーションシェル(HTML/CSS/JS)のキャッシュによる高速化。
- オフライン時の代替コンテンツ表示。
3. 技術選定の基準
各ストレージの特性を整理した比較表は以下の通りである。
| 手段 | 容量 | 寿命 | サーバー送信 | I/O | 主な用途 |
|---|---|---|---|---|---|
| Cookies | ~4KB | 設定依存 | あり(自動) | 同期 | 認証、サーバー共有設定 |
| LocalStorage | 5-10MB | 半永久 | なし | 同期 | UI設定、単純なKVS |
| SessionStorage | 5-10MB | タブ閉まで | なし | 同期 | 一時的なフォームデータ |
| IndexedDB | 大容量 | 半永久 | なし | 非同期 | 大量データ、複雑な構造 |
| Cache API | 大容量 | 半永久 | なし | 非同期 | 静的リソース |
選定フローチャート
4. データのライフサイクルと永続性
保存と破棄のタイミング
- 保存: 明示的なAPIコール(setItem等)またはサーバーレスポンス(Set-Cookie)によって行われる。
-
保持期間:
- SessionStorage: タブを閉じるまで。
- Cookie: 有効期限まで。
- LocalStorage / IndexedDB: ユーザーが削除操作を行うまで(原則永続)。
Eviction(自動削除)のリスク
「永続」とされているストレージであっても、ディスク容量が圧迫された場合、ブラウザは**LRU(Least Recently Used)アルゴリズム等に基づき、古いデータから自動的に削除(Eviction)**する場合がある。
特にモバイルデバイスではこの挙動が発生しやすいため、「ローカルデータは消える可能性がある」という前提で設計する必要がある。
5. 実装におけるベストプラクティス
① セキュリティ:機密情報の扱いは慎重に
XSS(クロスサイトスクリプティング)脆弱性が存在する場合、JavaScriptからアクセス可能なストレージ(LocalStorage, SessionStorage, document.cookie)内のデータは攻撃者に窃取されるリスクがある。
-
推奨: 認証トークン(JWT等)は、JavaScriptからアクセスできない
HttpOnly属性付きのCookie に保存する。 - 非推奨: LocalStorageにアクセストークンや個人情報を平文で保存する。
② パフォーマンス:メインスレッドをブロックしない
LocalStorageは同期APIである。巨大なJSONデータのシリアライズ(JSON.stringify)や書き込みは、UIの描画を阻害し「カクつき」の原因となる。
- 対策: データのサイズが大きい場合や、頻繁な書き込みが発生する場合は、非同期APIである IndexedDB の採用を検討する。
③ 堅牢性:QuotaExceededErrorのハンドリング
ストレージ容量の上限に達した場合や、プライベートブラウジングモードで書き込みが制限されている場合、保存処理は例外をスローする。
try {
localStorage.setItem('key', 'value');
} catch (e) {
if (isQuotaExceeded(e)) {
// 古いデータを削除するなどのリカバリ処理
}
}
④ 設計:スキーマバージョニング
アプリのアップデートにより保存すべきデータの構造が変更されることは頻繁にある。ユーザーのブラウザには古い構造のデータが残っているため、読み込み時に整合性が取れずエラーになることが多い。
対策: 保存データにバージョン番号を含め、読み込み時にマイグレーション処理を挟む。
const currentVersion = 2;
const savedData = JSON.parse(localStorage.getItem('settings'));
if (savedData && savedData.version < currentVersion) {
// マイグレーション処理、または初期化を実行
migrate(savedData);
}
まとめ
ブラウザのストレージ技術はそれぞれ明確な役割を持っている。
- サーバーとの連携が必要な識別子 → Cookie
- 簡易的な設定値 → LocalStorage
- 一時的な入力保持 → SessionStorage
- アプリケーションデータ → IndexedDB
それぞれの特性、特に同期/非同期の挙動やセキュリティリスクを正しく理解し、適材適所で選択することが重要である。