localStorage の使い方と、2026年に気をつけること

ブラウザにデータを保存する一番手軽な方法です。書き方は15年前から変わっていません。

変わったのは「何を入れていいか」のほうです。 解説記事の多くが「localStorageのデータには期限がない」と書いていますが、 Safariでは7日で消えます。これはAppleが公式に announced している仕様です。

この記事は基本の書き方と、知らないと事故る前提をまとめます。

先に結論

知りたいこと答え
保存localStorage.setItem('key', 'value')
取得localStorage.getItem('key')(無ければ null)
削除localStorage.removeItem('key') / 全消しは clear()
保存できる型文字列だけ(MDN:「キーと値は常に UTF-16 文字列形式」)
有効期限Safariは7日で消す。仕様としての期限は無い
容量おおむね5MB前後(ブラウザによる)
例外出る。必ず try/catch で包む
入れてはいけないもの消えて困るデータ、個人情報、トークン類

基本の4つ

setItem / getItem / removeItem / clear

これだけ覚えれば使えます。

基本操作

// 保存
localStorage.setItem('name', 'ランツ');

// 取得(キーが無ければ null)
const name = localStorage.getItem('name');

// 削除
localStorage.removeItem('name');

// 全部消す(同じオリジンの分だけ)
localStorage.clear();

// 件数と、n番目のキー
localStorage.length;
localStorage.key(0);

getItem() は、キーが無いとき null を返します。 空文字ではありません。if (value) だと保存された空文字と区別がつかないので、 厳密に見るなら if (value !== null) と書いてください。

オブジェクトは入らない

MDNにはこう書かれています。

localStorage に保存されるキーと値は 常に UTF-16 文字列形式です。

つまりオブジェクトや配列をそのまま渡すと、文字列に変換されて壊れます。

やったこと保存される値
setItem('a', {x: 1})"[object Object]"
setItem('a', [1, 2, 3])"1,2,3"(配列は区切り文字で連結)
setItem('a', 42)"42"(取り出すと文字列)
setItem('a', true)"true"(Boolean("false") は true なので注意)
setItem('a', null)"null"(文字列の "null"。getItem の null と紛らわしい)

JSONを経由してください。

オブジェクトを保存する

const memo = { title: 'buy milk', done: false };

// 保存:文字列にしてから
localStorage.setItem('memo', JSON.stringify(memo));

// 取得:戻すときにパース
const raw = localStorage.getItem('memo');
const restored = raw ? JSON.parse(raw) : null;

JSON.parse() は失敗すると例外を投げます。 保存した形式を変えたあと、古い形式のデータが残っているユーザーのところで落ちます。 パースも try/catch で包んで、失敗したらそのキーを捨てるのが安全です。

「期限がない」は、もう正しくない

MDNの説明はこうです。

localStorage は sessionStorage によく似ていますが、 localStorage のデータには期限がないのに対し、 sessionStorage のデータはページセッションが終了したとき、すなわちページが閉じられたときにクリアされます。

仕様としてはそのとおりです。でも実際のブラウザは消しにきます。

Safariは7日で消す

WebKitの公式ブログ(2020年3月24日・Safari 13.1 / iOS・iPadOS 13.4)に、こう書かれています。

7-Day Cap on All Script-Writeable Storage

…deleting all of a website's script-writable storage after seven days of Safari use without user interaction on the site.

(Safariを7日間使う間、そのサイトでユーザーの操作が無ければ、スクリプトから書き込まれたストレージを全部削除する)

対象として挙げられているのは次の5つです。

WebKitが挙げている対象よく使うもの
Indexed DBIndexedDB も対象
LocalStorageこの記事の主題
Media keys—
SessionStorage—
Service Worker registrations and cachePWAのキャッシュも対象

条件は「7日間Safariを使っていて、そのサイトでの操作が無い」こと。 毎日そのサイトを触っているユーザーのデータは消えません。 消えるのは「しばらく来ていないユーザー」のデータです。

IndexedDBに逃げても同じ

「容量が足りないからIndexedDBにする」「消えると困るからIndexedDBにする」は、後者については解決になりません。 上の一覧のとおり、IndexedDBも7日削除の対象です。 容量の問題は解決しますが、寿命の問題は解決しません。

ブラウザに持たせたデータを確実に残したいなら、サーバーに持つしかありません。 どうしてもクライアントに置くなら、消えても困らない設計にしてください。

何を入れてよくて、何を入れてはいけないか

入れていいもの理由
ダークモードの設定消えても既定値に戻るだけ
開いていたタブ・折りたたみの状態消えても実害がない
入力途中の下書き消えたら困るが、消える前提で「サーバーにも保存」できる
一度見た案内の既読フラグ最悪もう一度出るだけ
入れてはいけないもの理由
消えて困るデータ7日で消える。唯一の保存先にしない
認証トークン・セッションIDJavaScriptから読める=XSSで抜かれる。CookieのHttpOnlyを使う
個人情報共用端末で次の人に見られる。開発者ツールで誰でも読める
大きなデータ5MB前後で頭打ち。書き込みは同期なので画面が固まる

トークンの保存先としてlocalStorageを勧める記事が今も多いので、ここだけは強く書いておきます。 localStorage は同一オリジンのJavaScriptから無条件に読めます。 XSSが1か所でもあれば、そのページに読み込まれた任意のスクリプトが中身を持ち出せます。 Cookie の HttpOnly はJavaScriptから読めないので、ここが決定的に違います。

落ちるケース

localStorageは「必ず使える」ものではありません。 例外が出る条件がMDNに書かれています。

SecurityError が出る条件

SecurityError は以下のいずれかの場合に発生します。

  • オリジンが妥当なスキーム/ホスト名/ポート番号の組み合わせでない場合。
  • リクエストがポリシーの決定に反している場合(例えば、ユーザーが特定のオリジンにおいてデータの永続化を拒否している場合)。

実際に遭遇するのは後者です。 ブラウザの設定でサイトデータをブロックしている人、プライベートモード、 拡張機能でストレージを止めている人。一定数います。

容量を超えたとき

上限に達すると書き込み時に例外(QuotaExceededError)になります。 戻り値が false になるわけではありません。 気づかずに setItem() を呼び続けると、そこから下の処理が全部止まります。

必ず try/catch で包む

安全に読み書きする

const store = {
  get(key, fallback = null) {
    try {
      const raw = localStorage.getItem(key);
      return raw === null ? fallback : JSON.parse(raw);
    } catch {
      // ストレージ不可、または壊れたJSON
      return fallback;
    }
  },
  set(key, value) {
    try {
      localStorage.setItem(key, JSON.stringify(value));
      return true;
    } catch {
      // 容量超過・プライベートモード・ポリシーで拒否
      return false;
    }
  },
  remove(key) {
    try { localStorage.removeItem(key); } catch {}
  },
};

// 使う側は失敗を前提に書く
if (!store.set('draft', text)) {
  // 保存できなかったことをユーザーに伝えるか、黙って諦める
}

この形にしておくと、呼ぶ側が毎回 try/catch を書かなくて済みます。 get() が既定値を返すので、「ストレージが使えない環境でも動く」のが自然に実現できます。

sessionStorage / Cookie / IndexedDB の使い分け

似たものが4つあるので、選ぶ基準を表にします。

localStoragesessionStorageCookieIndexedDB
寿命明示的に消すまで(Safariは7日)タブを閉じるまで設定した期限まで明示的に消すまで(Safariは7日)
容量約5MB約5MB約4KB大きい
保存できる型文字列のみ文字列のみ文字列のみオブジェクト・Blob可
サーバーに送られる送られない送られない毎回送られる送られない
JSから読める読める読めるHttpOnlyなら読めない読める
API同期同期同期非同期
やりたいこと選ぶもの
設定・表示状態を覚えたいlocalStorage
そのタブの中だけで持ち回りたいsessionStorage
ログイン状態を持ちたいCookie(HttpOnly・Secure・SameSite)
画像やファイルを大量に持ちたいIndexedDB
絶対に消えては困るサーバー

localStorage と sessionStorage は同期APIです。 データ量が増えると、読み書きのあいだメインスレッドが止まります(=画面が固まる)。 数KB程度なら体感できませんが、ループの中で setItem() を何百回も呼ぶと分かるレベルで遅くなります。 まとめて1回に書くか、IndexedDBへ移してください。

他のタブと同期できる

あまり知られていませんが、localStorageの変更は他のタブに通知できます。 storage イベントです。

他タブでの変更を受け取る

window.addEventListener('storage', (e) => {
  // e.key        変更されたキー
  // e.oldValue   変更前の値
  // e.newValue   変更後の値(削除なら null)
  // e.storageArea 対象のStorageオブジェクト
  // e.url        変更を行ったページのURL
  if (e.key === 'memo:v1') {
    el.value = e.newValue ?? '';
  }
});

変更を行ったタブ自身では発火しません。 発火するのは同じオリジンの、他のタブ・ウィンドウです。 自分のタブでも処理したいなら、保存した直後に自分で呼ぶ必要があります。

デバッグ中に「イベントが来ない」と悩むのはたいていこれです。 タブを2つ開いて試してください。

プロパティ中身
key変更されたキー
oldValue変更前の値
newValue変更後の値(削除された場合は null)
storageArea対象の Storage オブジェクト
url変更を行ったページのURL

使いどころは「複数タブで同じ画面を開いている場合」です。 片方でログアウトしたらもう片方も画面を更新する、片方で下書きを消したら反映する、といった処理が書けます。

簡易メモ帳を作る

ここまでの注意点を全部入れた実装です。消える前提・落ちる前提で書いています。

HTML

<textarea id="memo" placeholder="メモ"></textarea>
<p id="status"></p>
<button id="clear">消す</button>

JavaScript

const KEY = 'memo:v1';          // ① キーにバージョンを付ける
const el = document.getElementById('memo');
const status = document.getElementById('status');

// 復元(失敗しても空で続行)
el.value = store.get(KEY, '');

// ② 入力のたびに書かない(まとめて1回)
let timer;
el.addEventListener('input', () => {
  clearTimeout(timer);
  timer = setTimeout(() => {
    const ok = store.set(KEY, el.value);
    // ③ 保存できたかをユーザーに見せる
    status.textContent = ok ? '保存しました' : '保存できませんでした';
  }, 500);
});

document.getElementById('clear').addEventListener('click', () => {
  store.remove(KEY);
  el.value = '';
  status.textContent = '';
});
やっていることこれが無いと
①キーに :v1 を付ける保存形式を変えたとき、古いデータでパースに失敗する。v2 にすれば旧データを無視できる
②入力の500ms後にまとめて保存同期APIを1文字ごとに叩く。長文で入力がもたつく
③保存の成否を表示プライベートモードで保存されていないのに保存した気になる

2011年にこの題材で書かれた記事との違いは、ほぼ全部この表です。 setItem と getItem の書き方自体は当時のままで通用します。 変わったのは、その周りに何を足すかです。

AIでやるなら

localStorageのコードはAIがすぐ書けます。問題は書けすぎることです。 何も言わないとtry/catchも無い、オブジェクトを直接入れる、トークンを保存するコードが出てきます。 学習データに2011年以降の記事が大量にあるからです。

前提を渡して書かせる

プロンプト

localStorageを使う小さなモジュールを書いてください。

【必ず守ること】
- 保存できるのは文字列だけなので JSON.stringify / JSON.parse を通す
- setItem も getItem も JSON.parse も try/catch で包む
- 保存に失敗したら true/false で呼び出し側に返す
- getItem は、キーが無いときと失敗したときに既定値を返す
- キーには "名前:v1" のようにバージョンを付ける

【書かないでほしいこと】
- 認証トークンやセッションIDを保存する例
- 「localStorageのデータは永続的です」という説明

【最後に】
このコードが動かない環境・消えるケースを箇条書きで挙げてください。

「書かないでほしいこと」が効きます。 とくにトークン保存はネット上に例が多すぎて、黙っていると出てきます。 先に封じてください。

最後の「消えるケースを挙げて」も入れてください。 プライベートモード・容量超過あたりは自分で挙げてくれるので、テスト項目がそのまま出てきます。 ただしSafariの7日削除は挙がらないことが多いので、そこは自分で足してください。

出てきたコードの検証

見るところNGなら何が起きるか
try/catch があるかプライベートモードでページ全体が止まる
JSON.stringify を通しているかオブジェクトが "[object Object]" になる
JSON.parse も囲ってあるか古い形式のデータが残っているユーザーで落ちる
トークンを保存していないかXSSで抜かれる
ループ内で setItem を連打していないか同期APIなので画面が固まる

実際に試すときは、この3つの環境で確認してください。

試す環境確認できること
プライベートウィンドウ例外で落ちないか
DevToolsで該当キーを手動で壊すJSON.parse の失敗に耐えるか
DevToolsのApplicationタブ何がどんな文字列で入っているか目で見る

よくある質問

localStorageのデータはいつまで残りますか
仕様上は明示的に消すまでですが、Safariは7日で消します。WebKitの公式ブログに「after seven days of Safari use without user interaction on the site」と明記されています。消える前提で設計してください。
IndexedDBに移せば消えませんか
消えます。WebKitが挙げている7日削除の対象にIndexedDB も入っています(SessionStorage、Service Workerのキャッシュも)。容量の問題は解決しますが、寿命の問題は解決しません。
オブジェクトが [object Object] になります
文字列しか保存できないためです。MDNに「キーと値は常に UTF-16 文字列形式」とあります。JSON.stringify() を通してから保存し、取り出すときに JSON.parse() してください。
ログイン状態の保存に使えますか
やめてください。localStorage は同一オリジンのJavaScriptから無条件に読めるので、XSSが1か所でもあれば抜かれます。Cookie の HttpOnly ならJavaScriptから読めません。こちらを使ってください。
プライベートモードで動きません
MDNによると、ユーザーがそのオリジンでのデータ永続化を拒否している場合に SecurityError が発生します。プライベートモードや、サイトデータをブロックしている設定が該当します。try/catch で包んで、使えない前提で動くようにしてください。
容量はどれくらいですか
おおむね5MB前後です(ブラウザによります)。超えると書き込み時に例外が出ます。戻り値では分からないので、try/catch が要ります。
sessionStorage との違いは
MDNの表現では、localStorageのデータには期限がないのに対し、sessionStorageはページセッションが終了したとき(ページが閉じられたとき)にクリアされます。APIは同じです。

まとめ

書き方は15年前から変わっていません。押さえるのは3つです。

  1. 文字列しか入らない。
    JSON.stringify / JSON.parse を通す。パースも try/catch で囲む。
  2. 「期限がない」は実態と違う。
    Safariは7日で消します。IndexedDBに移しても同じです。消えて困るものはサーバーへ。
  3. 落ちる前提で書く。
    プライベートモードと容量超過で例外が出ます。トークンは絶対に入れない。

解説記事をコピーする前に、それがいつ書かれたものかを見てください。 APIが変わっていなくても、前提のほうが変わっていることがあります。

SSRで localStorage is not defined が出る話は、原因がまったく別なので localStorage is not defined の直し方 に分けて書きました。 Next.js で "use client" を付けても直らない理由はそちらです。
同じ型の話を PHPでOGPを取得する にも書きました。 あちらは標準APIが新しくなったのに古いライブラリが勧められ続けているケースです。 JavaScriptで要素を取る話は jQuery で自分自身の要素を取得する に。

公式情報は2026年10月25日時点で MDN と WebKit公式ブログから確認したものです。 ブラウザのストレージ方針は変わります。 重要なデータを扱う前に、各ブラウザの最新の挙動を確認してください。