マガジン

localStorageが保存されない:プライベートモードと容量の壁

遊びをつくる

執筆にClaudeを使用しています。

机上の二つの端末の間に、小さなゲームの駒と記録カードが並ぶイラスト。
記事のテーマをもとに制作したAI生成イメージ。実際のゲーム画面ではありません。

ブラウザゲームを作って動作確認したとき、「スコアが保存されない」「ステージ進行がリセットされる」という現象に出くわすことがあります。コードを見直しても構文エラーはなく、localStorage.setItem() は呼ばれているはずなのに、再読み込みするとデータが消えている。この記事では、その原因として頻度が高い「プライベートブラウジング」と「容量超過」の二つに絞り、判断の仕方と対処の手順を説明します。 原因が一つとは限らないため、順番に切り分けることが大切です。まずプライベートモードかどうかを確認し、次に容量の問題を調べる、という流れで進めてください。

この記事の目次
  1. プライベートモードでは保存が「ゼロか一時か」になる
  2. 容量超過は例外で知らせてくれる
  3. 二つ以外に疑うべき点:オリジンの不一致
  4. 保存の失敗をユーザーに伝える設計
  5. 次の行動

プライベートモードでは保存が「ゼロか一時か」になる

Chromeのシークレットウィンドウ、FirefoxやSafariのプライベートウィンドウでは、localStorage の扱いがブラウザによって異なります。共通して言えるのは、「ウィンドウを閉じると消える」か「そもそも書き込みが例外を投げる」かのどちらかだということです。

具体的な挙動はブラウザのバージョンによって変わるため断定はできませんが、開発者が押さえておくべき考え方は次の通りです。

  • セッションをまたいだ保存を前提とするデータ(スコア、進捗)はプライベートモードで動作しないことがある
  • 例外が投げられるケースでは、try/catch がなければスクリプト全体が止まる可能性がある

これを踏まえると、ゲームの保存処理は最低限 try/catch で囲んでおく必要があります。

js
try {
  localStorage.setItem('score', JSON.stringify(score));
} catch (e) {
  console.warn('保存できませんでした:', e);
}

このように書いておくと、プライベートモードで例外が出てもゲームが止まらず、ユーザーには「保存できませんでした」と案内することができます。

自分の環境がプライベートモードかを確認する方法

デバッグ中に「なぜか保存されない」と感じたら、まずブラウザのアドレスバー左端のアイコンやウィンドウの色を確認してください。シークレット・プライベートウィンドウには視覚的な区別があります。または、ブラウザの開発者ツール(F12)→「Application」(Chromeの場合)→「Local Storage」を開いて、保存したキーが実際に書き込まれているかを直接確認するのが確実です。書き込みが失敗していれば、そこにキーは表示されません。

容量超過は例外で知らせてくれる

localStorage には上限容量があり、一般的にはオリジン(同じドメイン)ごとに数MBとされています。ゲームの進行データや設定程度であれば通常は問題になりませんが、画像データをBase64に変換して保存しようとしたり、ログを大量に積み重ねたりすると上限に達することがあります。

容量を超えた書き込みを試みると、ブラウザは QuotaExceededError(または NS_ERROR_DOM_QUOTA_REACHED)という例外を投げます。この例外は try/catch で捕捉できます。

js
try {
  localStorage.setItem('mapData', largeString);
} catch (e) {
  if (e.name === 'QuotaExceededError' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED') {
    console.warn('容量超過:古いデータを削除してください');
  }
}

容量超過が疑われるときは、開発者ツールのコンソールで次のコードを実行すると現在の使用量をざっくり確認できます。

js
let total = 0;
for (let key in localStorage) {
  if (localStorage.hasOwnProperty(key)) {
    total += (localStorage[key].length + key.length) * 2;
  }
}
console.log('使用量(概算):', (total / 1024).toFixed(1), 'KB');

これはあくまで文字数から算出する概算ですが、数百KBを超えているようなら設計を見直す目安になります。

容量を抑えるための判断基準

保存するデータが大きくなる場合は、以下の観点で削れるものを削ってください。

  1. 画像やバイナリをlocalStorageに入れない 画像は<img>タグやCSSで管理し、localStorageにはファイルパスや識別子だけを保存します。
  2. ログや履歴は件数を上限で管理する 「直近50件だけ保持する」などの上限を設けてシフトします。
  3. JSONはキーを短くする {"playerMaxScore": 9900} より {"pms": 9900} の方が蓄積すると差が出ます。ただし可読性とのバランスを考えて判断してください。
  4. 不要なキーをこまめに削除する 機能を作り直した際に古いキーが残りがちです。localStorage.removeItem() や開発者ツールからの手動削除で整理しておきます。

二つ以外に疑うべき点:オリジンの不一致

プライベートモードでも容量超過でもなく保存されない場合、file:// プロトコルで開いていることが原因のケースがあります。HTMLファイルをローカルで直接ダブルクリックして開くと、アドレスバーに file:///Users/... と表示されます。この状態では、ブラウザによって localStorage が完全に使えないか、起動のたびに分離されるかのどちらかになることがあります。

動作確認はローカルサーバーを立てて http://localhost:... 経由で行うか、開発用のサービスにデプロイして確認するのが確実です。VSCodeを使っているなら「Live Server」拡張が手軽です。

保存の失敗をユーザーに伝える設計

エラーを握りつぶさず、ユーザーに状況を伝えることも重要です。「オートセーブできませんでした」という一言をゲーム画面に表示するだけで、プレイヤーは「ブラウザの設定の問題かもしれない」と自分で気づくことができます。何も表示しないと、バグだと思ってゲームを閉じてしまいます。

実装の例としては、保存処理の catch ブロックでゲーム内のUIに通知を出す関数を呼ぶ形が簡潔です。通知の表示は数秒で消えるトースト形式が多く使われています。

保存が成功したかどうかを確認したい場合は、書き込んだ直後に localStorage.getItem() で読み返し、期待する値と一致するかをチェックする方法があります。容量超過が起きた場合は setItem が例外を出すので、getItem まで到達しないことで区別できます。

次の行動

まずブラウザの開発者ツール(Application → Local Storage)を開き、書き込みが実際に行われているかを目で確認してください。書き込まれていなければ、保存処理全体を try/catch で囲み、例外の name をコンソールに出してから原因を特定します。ゲームが完成してアソバースに投稿する際も、プレイヤーがどのブラウザのどの状態で開くか分からないため、保存失敗時の案内は入れておくことをおすすめします。