Microsoft EdgeのlocalStorageでQuotaExceededErrorが出る原因と対処法完全ガイド【5MB上限・IndexedDB移行】

Microsoft Edge で突然 localStorage の保存に失敗し、ユーザー設定や編集中データが消える――フロントエンドで最も避けたい事故のひとつです。本稿では、エラーの正体(QuotaExceededError)と原因、現場で即実践できる応急処置から恒久対策(圧縮・データ設計・IndexedDB への移行・PWA の永続化・File System Access など)まで、手順とコード例をまとめて解説します。ブラウザ依存の落とし穴や InPrivate での厳格な制限も併せて押さえましょう。

目次

Edge の localStorage で発生するエラーの概要

Microsoft Edge(Chromium 系)で localStorage.setItem() を実行した際、保存容量の上限に達すると次の例外が投げられます。

Failed to execute 'setItem' on 'Storage': Setting the value of 'userData' exceeded the quota.

ブラウザが確保している「オリジンごとの割り当て」を超過すると、この QuotaExceededError が発生し、以後の書き込みは失敗します。症状としては次のようなものが典型です。

  • アプリの設定(テーマ、レイアウト、フィルター条件など)が保存できない/保存直後に消える
  • 編集中の下書きが消える、フォームのドラフトが復元できない
  • ユーザーから「保存ボタンを押したのに反映されない」と報告が来る

原因の理解:localStorage の容量は「およそ 5 MB」

Edge を含む主要ブラウザは、同一オリジン(プロトコル+ホスト+ポート)ごとに、およそ 5 MB 前後の localStorage 上限を設けています。厳密な値やカウント方法は実装に依存しますが、一般にはキーと値の合計サイズが基準です。

  • 実容量は文字数ではなく「バイト」換算です。UTF-16/UTF-8 の扱い、メタデータなど実装差があるため、同じ文字数でも実バイトは前後します。
  • InPrivate(シークレット)モードでは、割当てがさらに厳しくなったり、セッション終了時にストレージが消去されるため、同じコードでも急にエラーが出やすくなります。
  • OS のディスク空き容量が乏しい場合や、企業ポリシーでストレージが制限されている環境では、利用可能容量が実質的に縮むことがあります。

質問の状況(要約)

アプリが Microsoft Edge で localStorage.setItem() を実行したところ、Failed to execute 'setItem' on 'Storage': Setting the value of 'userData' exceeded the quota. という例外が発生し、ユーザー設定や編集中データが保存できなくなった。

最初に試す「応急処置」手順(実作業の流れ)

手順内容
原因の理解Edge を含む主要ブラウザでは、同一オリジンにつき約 5 MB 程度の localStorage 容量制限がある。これを超えると QuotaExceededError が発生する。
不要データの削除1. F12 キーで DevTools を開く → Application タブ → Local Storage
2. 容量の大きいキーや不要キーを削除し、再実行する。
保存データの最適化・文字列を圧縮(例:LZ-string)、キー名や JSON プロパティ名を短くする。
・画像・大きなバイナリは Base64 で保存せず、IndexedDB や外部ストレージへ移行する。
ストレージ種別の切替5 MB を超える継続的データは IndexedDB、一時データは sessionStorage など、用途に応じてストレージを分ける。
ブラウザモード確認シークレット(InPrivate)モードはさらに厳しい容量制限があるため、通常ウィンドウでの動作確認を推奨。
端末環境の確認ローカルディスクの空き容量不足や、組織ポリシーによるストレージ制限がないか確認する。

容量を超えているか素早く見極める:計測スニペット

localStorage の「実」使用量をおおまかに把握するには、各キーのサイズを合算するユーティリティが役立ちます。

// localStorage の概算バイト数を計算(キーと値を合算)
function localStorageSizeBytes() {
  let total = 0;
  for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    const value = localStorage.getItem(key);
    // Blob によるバイト推定(実装差による誤差はあり)
    total += new Blob([key]).size + new Blob([value]).size;
  }
  return total;
}

// 目安表示(MB)
console.log((localStorageSizeBytes() / (1024 * 1024)).toFixed(2) + ' MB');

// Storage API でサイト全体の quota/usage を把握(IndexedDB なども含む推定)
if (navigator.storage && navigator.storage.estimate) {
navigator.storage.estimate().then(e => {
const used = (e.usage || 0) / (1024 * 1024);
const quota = (e.quota || 0) / (1024 * 1024);
console.log(`Site usage: ${used.toFixed(2)}MB / quota ${quota.toFixed(2)}MB`);
});
} 

注意:navigator.storage.estimate() はサイト全体の見積りであり、localStorage 単独の上限値とは一致しませんが、「もう限界に近いか?」 の判断材料として有用です。

エラーを再現して原因を確信する(デバッグ用)

再現コードをコンソールで実行し、エラーが QuotaExceededError であることを確認します。

// 100KB の擬似データを作り、繰り返し書き込む
function fillLocalStorage() {
  const payload = 'x'.repeat(100 * 1024);
  let i = 0;
  try {
    for (;; i++) {
      localStorage.setItem('test:' + i, payload);
    }
  } catch (err) {
    console.error('Stop at i=', i, err.name, err.message);
  }
}
fillLocalStorage();

Edge で上記を実行し、一定回数で QuotaExceededError が出れば、容量超過が主因と判断できます。

堅牢化:安全な保存ラッパー(try/catch とフォールバック)

実運用では、保存時に例外が起きてもアプリが壊れないように、安全な setItem ラッパーを導入しましょう。

/**
 * localStorage に安全に保存する(失敗時は圧縮や退避を試みる)
 */
async function safeSetItem(key, value, {compress = false, onFallback} = {}) {
  const text = typeof value === 'string' ? value : JSON.stringify(value);
  const toSave = compress && window.LZString ? LZString.compressToUTF16(text) : text;

try {
localStorage.setItem(key, toSave);
return {ok: true, savedAs: 'localStorage', compressed: !!compress};
} catch (e) {
if (e && (e.name === 'QuotaExceededError' || e.code === 22)) {
// 退避先へフォールバック(IndexedDB など)
if (typeof onFallback === 'function') {
await onFallback(key, text);
return {ok: true, savedAs: 'fallback', compressed: false};
}
}
console.error('safeSetItem failed', e);
return {ok: false, error: e};
}
} 

ポイントは次のとおりです。

  • 例外を握りつぶさない(ログを残し、UI に「保存方法を切り替えました」などの通知を出す)
  • 必要に応じて圧縮(LZ-string などを利用。導入時はパフォーマンス計測を推奨)
  • フォールバックとして IndexedDB などに退避し、ユーザーの作業継続を最優先する

保存データをスリム化するための具体策

  • JSON を最短化:プロパティ名の省略、無駄な空白や改行を入れない(JSON.stringify(obj) の第 3 引数にスペースは渡さない)。
  • 差分保存:履歴やログを丸ごと保存せず「最新のみ」「直近 N 件のみ」に制限。
  • Base64 を避ける:画像やバイナリを Base64 化するとおよそ 33% 程度サイズが増えます。IndexedDB の BlobFile System Access API で扱う方が効率的です。
  • 圧縮:LZ-string(compressToUTF16/decompressFromUTF16)などの軽量圧縮を検討。ただし CPU コストとトレードオフのため、対象データを絞る。
  • TTL と自動削除:保存時に expiresAt を持たせ、起動時に古いキーを掃除(エビクション)する。

用途別:どのストレージを使うべきか

用途推奨ストレージ理由容量の目安
ユーザー設定・トグルlocalStorage即時同期・読み出しが軽い〜数十 KB
フォームのドラフト(短期)sessionStorageタブ単位。閉じると消えるため漏洩リスクを抑制〜数百 KB
一覧キャッシュ・検索結果IndexedDB非同期で大容量、Blob も扱えるMB〜GB(環境に依存)
画像・添付ファイルIndexedDB / File System AccessBase64 を避けて効率保存サイズ依存
オフライン編集の元データIndexedDB + 永続化PWA と相性が良い。エビクション耐性向上数十 MB 以上

IndexedDB への移行:ミニマル実装例

外部ライブラリなしで JSON データを保存・取得する基本形です。

// DB を開く(初回はオブジェクトストアを作成)
function openDB() {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open('appDB', 1);
    req.onupgradeneeded = () => {
      const db = req.result;
      if (!db.objectStoreNames.contains('kv')) db.createObjectStore('kv');
    };
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

async function idbSet(key, value) {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction('kv', 'readwrite');
tx.objectStore('kv').put(value, key);
tx.oncomplete = () => resolve(true);
tx.onerror = () => reject(tx.error);
});
}

async function idbGet(key) {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction('kv', 'readonly');
const req = tx.objectStore('kv').get(key);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}

// localStorage が溢れたら IndexedDB へ退避
safeSetItem('userData', bigJson, {
compress: false,
onFallback: (key, raw) => idbSet(key, raw)
}); 

画像・大きなバイナリの扱い:Base64 ではなく Blob/ファイルへ

大きな画像を Base64 で localStorage に入れると、容量を一気に食い、しかも 33% ほどサイズ増になります。IndexedDB に Blob として保存するか、File System Access API でユーザーの許可のもとにファイルへ保存しましょう。

// IndexedDB に Blob を保存
async function saveImageBlobToIdb(blob, key) {
  const db = await openDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction('kv', 'readwrite');
    tx.objectStore('kv').put(blob, key);
    tx.oncomplete = () => resolve(true);
    tx.onerror = () => reject(tx.error);
  });
}

// 表示するときは Object URL を使う
async function loadImageUrlFromIdb(key) {
const blob = await idbGet(key);
return URL.createObjectURL(blob); // img.src にセット
} 

InPrivate(シークレット)モードの落とし穴

  • 割当てが通常より厳格で、短時間で上限に達しやすい
  • セッション終了時にデータが消えるため、永続ストレージとしては不適
  • 検証時は通常ウィンドウInPrivate の双方で挙動を確認し、エラー再現の有無を切り分ける。

企業環境・組織ポリシーの影響

企業端末では、ブラウザのストレージ関連ポリシーや「終了時に閲覧データを削除」設定が有効化されていることがあります。症状としては「保存できたりできなかったりする」「再起動すると消える」などの断続的なトラブルです。IT 管理者向けに、次の観点を共有しましょう。

  • ブラウザ終了時の自動消去ポリシーが有効か
  • プロファイルのローミング/再プロビジョニングにより、ストレージがリセットされていないか
  • ディスク残量が閾値を下回っていないか(仮想環境・VHDX・プロファイルコンテナ等)

永続化(PWA 向け):エビクションを避ける工夫

オフラインファーストな PWA では、アセットやデータを大量に保存します。OS の空き容量低下やブラウザのハウスキーピングでキャッシュが消えるのを抑えるため、永続化のリクエストを検討します。

// 可能ならストレージ永続化を要求(ユーザーの許可が必要)
async function ensurePersistence() {
  if (navigator.storage &amp;&amp; navigator.storage.persist) {
    const isPersisted = await navigator.storage.persisted();
    if (!isPersisted) {
      const granted = await navigator.storage.persist();
      console.log('Persistence', granted ? 'granted' : 'denied');
    }
  }
}

また、ユーザーがローカルにファイルとして保存・読み込みできるようにすると、ストレージ容量の制限を根本的に回避できます(File System Access API)。

// JSON をローカルファイルへ保存(ユーザー操作が必要)
async function exportJsonToFile(filename, obj) {
  const handle = await window.showSaveFilePicker({
    suggestedName: filename,
    types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }]
  });
  const writable = await handle.createWritable();
  await writable.write(new Blob([JSON.stringify(obj)], { type: 'application/json' }));
  await writable.close();
}

衝突回避:マルチタブの同時書き込み

localStorage は同期 I/Oで、書き込みはブロック的に実行されます。複数タブが同一キーに書き込むと、最後に書いた方が勝つ「ロストアップデート」が起きがちです。最低限、storage イベントでの協調を入れておきましょう。

// 他タブの変更を受けて UI を更新
window.addEventListener('storage', (e) => {
  if (e.key === 'userData') {
    // e.newValue を UI に反映
  }
});

// 争いを減らす:キーをシャーディングし、局所的な上書きを避ける
// 例: userData:profile, userData:filters, userData:layout 

自動掃除(エビクション)と TTL の設計例

const MAX_ITEMS = 200; // 例:保持する履歴上限

function putWithMeta(key, value, ttlMs) {
const item = { value, ts: Date.now(), exp: ttlMs ? Date.now() + ttlMs : null };
localStorage.setItem(key, JSON.stringify(item));
}

function getWithMeta(key) {
const raw = localStorage.getItem(key);
if (!raw) return null;
const item = JSON.parse(raw);
if (item.exp && Date.now() > item.exp) {
localStorage.removeItem(key);
return null;
}
return item.value;
}

function vacuum(prefix) {
const entries = [];
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
if (k.startsWith(prefix)) {
const it = JSON.parse(localStorage.getItem(k));
entries.push({k, ts: it.ts || 0});
}
}
entries.sort((a, b) => b.ts - a.ts);
for (let i = MAX_ITEMS; i < entries.length; i++) {
localStorage.removeItem(entries[i].k);
}
} 

LZ-string を使った圧縮・解凍(導入例)

文字列データに偏りがあり、重複が多いときは圧縮が効きます。導入時は CPU/メモリコストを計測しましょう。

// 圧縮保存
function setCompressed(key, obj) {
  const raw = typeof obj === 'string' ? obj : JSON.stringify(obj);
  const comp = LZString.compressToUTF16(raw);
  localStorage.setItem(key, comp);
}

// 解凍取得
function getCompressed(key) {
const comp = localStorage.getItem(key);
if (comp == null) return null;
const raw = LZString.decompressFromUTF16(comp);
try { return JSON.parse(raw); } catch { return raw; }
} 

「安全に落ちる」UI 設計:ユーザー体験を壊さない

  • 保存が失敗したらトースト通知で理由と対処(「容量が上限に達しました。履歴の削除またはファイル保存をご利用ください」)を伝える。
  • エクスポート/インポートボタンを常設して、退避ルートを確保する。
  • 同じキーへ断続的に書き込まない(デバウンス/スロットリング)。
  • 書き込み前にサイズ見積もりを行い、限界の手前でユーザーに選択肢を提示する。

ブラウザ別の傾向(目安)

ブラウザlocalStorage 上限の目安InPrivate/シークレット所感
Microsoft Edge (Chromium)約 5 MBより厳格/短命Chromium 系と同等。企業ポリシーの影響に注意
Google Chrome約 5 MBより厳格/短命実運用でも 5 MB 目安を越えない設計が安全
Firefox約 5 MB(実装差あり)より厳格計測ロジックを入れて挙動差を吸収
Safari約 5 MB(バージョン差あり)より厳格iOS Safari はシステム都合で消えることがある

前提として、各ブラウザは「サイト全体のストレージ(IndexedDB などを含む)」に対する動的な割り当てやガベージ回収を持ち、細部は変わり得ます。「5 MB はあくまで安全ライン」と捉え、超える可能性があるデータは最初から IndexedDB に逃がすのが実務的です。

実務フロー:現場での切り分けから恒久対策まで

  1. 再現:該当ユーザー環境で DevTools を開き、コンソールに再現コードを投入。例外名・メッセージ・堆積状況を確認。
  2. 掃除Local Storage の不必要なキー(古いバージョンのキャッシュ、ログ、巨大な Base64 画像など)を削除。
  3. 監視localStorageSizeBytes() と Storage API でおおまかな使用量をダッシュボード表示。
  4. 縮小:キー名の短縮・JSON の正規化・圧縮の A/B 計測。
  5. 設計変更用途別ストレージ に切り分け、大きいデータは IndexedDB へ移行。
  6. PWA 最適化:永続化の要求、アセットのキャッシュ戦略(Stale-While-Revalidate など)を調整。
  7. エクスポート経路:ファイル保存によるバックアップ導線を UI へ常設。

QA:よくある質問

Q. localStorage の 5 MB を超えて保存する方法は?
A. ありません。実装依存で多少の上下はあっても、設計としては超えない前提にし、IndexedDB もしくは 外部ファイル/クラウドに逃がします。

Q. どのデータから移行すべき?
A. 体感劣化を生みやすい「大きく頻繁に更新されるデータ」から。例えば検索結果キャッシュ、エディタのドラフト、画像プレビューなどは IndexedDB 向きです。

Q. 圧縮は常に有効?
A. いいえ。短い JSON や乱数っぽいデータは圧縮効率が悪く、CPU コストの方が上回ることがあります。対象を選ぶのがコツです。

Q. InPrivate でだけ落ちるのはバグ?
A. 仕様上の制限によるものが多いです。InPrivate をサポート対象外とするか、保存量を最小限にして「落ちない設計」にします。

チェックリスト(コピー&ペーストでチーム共有)

  • [ ] localStorage に画像・巨大文字列を保存していないか
  • [ ] JSON プロパティ名・キー名が冗長でないか
  • [ ] 書込前に概算サイズを評価しているか
  • [ ] 失敗時のフォールバック(IndexedDB 等)が実装されているか
  • [ ] TTL と自動掃除の仕組みがあるか
  • [ ] InPrivate での挙動をテストしているか
  • [ ] 企業端末ポリシーの影響を確認したか
  • [ ] エクスポート/インポート経路を UI に用意したか
  • [ ] PWA の永続化(persist)を試しているか

テスト戦略:CI で「うっかり肥大化」を防ぐ

ユニットテストや E2E テストに「サイズが閾値を超えたら失敗する」アサーションを組み込みます。

// 疑似 localStorage を使ったサイズガード(例)
function assertWithinLocalStorageLimit(obj, limitBytes = 4.5 * 1024 * 1024) {
  const serialized = JSON.stringify(obj);
  const bytes = new Blob([serialized]).size;
  if (bytes &gt; limitBytes) {
    throw new Error(`localStorage budget exceeded: ${bytes} bytes`);
  }
}

「機能追加で設定オブジェクトが太る」などの回帰を、早期に検知できます。

まとめ:5 MB の「壁」を設計で越えない

  • localStorage はおよそ 5 MB。上限を前提に薄く短く使う。
  • 巨大データはIndexedDBファイルへ。Base64 は避ける。
  • 例外を握らず safeSetItem でフォールバック。TTL と掃除で肥大化を防止。
  • PWA では永続化要求キャッシュ戦略でエビクション耐性を高める。
  • InPrivate/企業ポリシーの差異をテストに組み込み、ユーザー環境での再現性を担保する。

作業時間の目安(今回のケース)

不要キーの削除と動作テストで5〜10 分程度。恒久対策(圧縮・設計見直し・IndexedDB への移行)は、データ量と影響範囲に応じて段階的に進めると安全です。

補足:パフォーマンスと PWA の回避策

  • localStorage は同期 I/Oのため、大容量データを頻繁に書き込むとメインスレッドをブロックし、描画パフォーマンスに影響することがあります。
  • PWA であれば File System Access API でローカルファイルに保存する、あるいはクラウド同期ストレージを併用することでクォータ問題を回避可能です。

実装テンプレート集(すぐ使える断片)

保存前のヘルスチェック

function canSaveToLocalStorage(nextBytes, threshold = 4.5 * 1024 * 1024) {
  try {
    const current = localStorageSizeBytes();
    return (current + nextBytes) &lt; threshold;
  } catch {
    return false;
  }
}

フォーム下書き:セーフティ付き保存

async function saveDraft(formState) {
  const raw = JSON.stringify(formState);
  const bytes = new Blob([raw]).size;

if (!canSaveToLocalStorage(bytes)) {
// 閾値超過なら IndexedDB へ
await idbSet('draft', raw);
return 'idb';
}
try {
localStorage.setItem('draft', raw);
return 'ls';
} catch (e) {
await idbSet('draft', raw);
return 'idb';
}
} 

巨大ログの分割保存(シャーディング)

function saveLogSharded(prefix, text, chunkSize = 64 * 1024) {
  // 既存チャンクを掃除
  for (let i = 0;; i++) {
    const k = `${prefix}:${i}`;
    if (!localStorage.getItem(k)) break;
    localStorage.removeItem(k);
  }
  // チャンク化して保存
  for (let i = 0; i &lt; text.length; i += chunkSize) {
    const chunk = text.slice(i, i + chunkSize);
    localStorage.setItem(`${prefix}:${i / chunkSize}`, chunk);
  }
  localStorage.setItem(`${prefix}:chunks`, String(Math.ceil(text.length / chunkSize)));
}

Base64 からの脱却:ファイル保存のガイド

async function saveDataAsFile(name, data) {
  const handle = await window.showSaveFilePicker({ suggestedName: name });
  const writable = await handle.createWritable();
  await writable.write(new Blob([data]));
  await writable.close();
}

運用ノート:ログ収集とユーザー支援

  • 匿名の容量メトリクスを収集(平均・P90・P99 の使用量)し、リリース前に安全域を可視化。
  • 復旧ガイドをアプリ内に設ける(「設定 → ストレージのクリーンアップ」「バックアップのエクスポート」)。
  • サポートチーム向けに「DevTools の開き方」と「Local Storage の削除手順」を手順書化。

最後に:設計原則のひとこと

localStorage は「メモ付きスイッチ」くらいに留める。――これだけで多くの事故は避けられます。大きなデータは最初から大きな器(IndexedDB / ファイル)へ。限界を設計に組み込めば、QuotaExceededError は怖くありません。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次