はじめに
Webアプリケーションのクライアント側ストレージは、オフラインキャッシュやユーザー設定、下書きデータ、セッション状態の保持に欠かせません。長らくlocalStorageが最もシンプルな選択肢でしたが、アプリケーションの大規模化に伴いその限界が顕在化しています。本記事ではlocalStorage、生のIndexedDB、そしてlocalForageラッパーライブラリを比較し、適切なストレージ選択の指針を提供します。
localStorageの限界
localStorageは同期的なキー・バリューAPI(setItem / getItem)を提供しますが、以下の強い制約があります。
| 制約 | 詳細 |
|---|---|
| 最大容量 | オリジンあたり約5 MB(ブラウザにより変動) |
| 同期的API | 読み書き中はメインスレッドをブロック |
| 文字列のみ | オブジェクトの保存にはJSONシリアライズが必須 |
| 索引機能なし | データのクエリやフィルタが不可能 |
SPAで大規模なオフラインキャッシュや複雑なアプリケーション状態を扱うと、5 MBの壁にすぐに到達します。また同期書き込みによるレンダリングブロックは、特に低性能端末でユーザー体験を著しく損ねます。
IndexedDB: 高性能だが複雑
IndexedDBはブラウザ組み込みのNoSQLデータベースです。以下の特長を持ちます。
- 事実上無制限のストレージ(ブラウザ依存、通常数百MB〜GB)
- 完全非同期API — UIスレッドをブロックしない
- 構造化データ — オブジェクト、Blob、配列をそのまま保存可能
- インデックスによる効率的な検索
しかし生のIndexedDBはコード量が非常に多いという問題があります。
const request = indexedDB.open('MyDatabase', 1);
request.onsuccess = (event) => {
const db = event.target.result;
const tx = db.transaction('store', 'readonly');
const store = tx.objectStore('store');
const get = store.get('myKey');
get.onsuccess = () => console.log(get.result);
};
単純なキー・バリュー操作にもかかわらず複数のイベントハンドラが必要で、初心者には敷居が高いAPIです。
localForage: 両方の良さを活かす
localForageはIndexedDB(フォールバックとしてWebSQLおよびlocalStorage)を、localStorageのようなシンプルな非同期APIでラップする軽量ライブラリです。利用可能な最適なストレージエンジンを自動選択します。
npm install localforage
基本的な使い方
import localforage from 'localforage';
// 保存(オブジェクトやBlobもそのまま保存可能)
await localforage.setItem('user', { name: '山田', role: 'admin' });
// 読み取り
const user = await localforage.getItem('user');
console.log(user.name); // "山田"
// 削除
await localforage.removeItem('user');
// 全削除
await localforage.clear();
PromiseベースのAPIで、async/awaitと相性が良く、コードが簡潔になります。
設定例
localforage.config({
driver: [localforage.INDEXEDDB, localforage.LOCALSTORAGE],
name: 'MyApp',
storeName: 'app_cache',
});
フォールバックの仕組み
localForageは以下の優先順位でドライバを自動選択します。
- IndexedDB — 容量・非同期性能ともに最優先
- WebSQL — 非推奨だが一部ブラウザで利用可能
- localStorage — 最終手段、5 MB制限あり
実例: APIレスポンスのキャッシュ
import localforage from 'localforage';
const CACHE_TTL = 5 * 60 * 1000; // 5分
async function fetchWithCache(url) {
const cached = await localforage.getItem(url);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}
const response = await fetch(url);
const data = await response.json();
await localforage.setItem(url, { data, timestamp: Date.now() });
return data;
}
このパターンによりネットワークリクエストを削減しつつ、UIの応答性を維持できます。
適切なストレージの選び方
| ユースケース | 推奨ストレージ |
|---|---|
| 単純なキー・バリュー、小容量(5 MB未満) | localStorage / localForage |
| 大規模JSONキャッシュ、オフラインデータ | localForage(IndexedDBバックエンド) |
| 複雑なクエリ、インデックス検索 | 生のIndexedDB |
| タブ間データ同期 | IndexedDB + BroadcastChannel |
| 一時的なセッション情報 | sessionStorage |
まとめ
localStorageはごく小規模なデータには今でも有効ですが、同期的な制約と5 MBという容量制限から、データ量の多いモダンなアプリケーションには不向きです。生のIndexedDBは強力ですが、キー・バリュー用途には過剰に複雑です。localForageはPromiseベースのクリーンなAPIでIndexedDBの機能を活かし、環境に応じてgracefulにフォールバックします。ほとんどのクライアントストレージ要件において、localForageは実用的な選択肢となるでしょう。
