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 DB | IndexedDB も対象 |
| LocalStorage | この記事の主題 |
| Media keys | — |
| SessionStorage | — |
| Service Worker registrations and cache | PWAのキャッシュも対象 |
条件は「7日間Safariを使っていて、そのサイトでの操作が無い」こと。 毎日そのサイトを触っているユーザーのデータは消えません。 消えるのは「しばらく来ていないユーザー」のデータです。
IndexedDBに逃げても同じ
「容量が足りないからIndexedDBにする」「消えると困るからIndexedDBにする」は、後者については解決になりません。 上の一覧のとおり、IndexedDBも7日削除の対象です。 容量の問題は解決しますが、寿命の問題は解決しません。
ブラウザに持たせたデータを確実に残したいなら、サーバーに持つしかありません。 どうしてもクライアントに置くなら、消えても困らない設計にしてください。
何を入れてよくて、何を入れてはいけないか
| 入れていいもの | 理由 |
|---|---|
| ダークモードの設定 | 消えても既定値に戻るだけ |
| 開いていたタブ・折りたたみの状態 | 消えても実害がない |
| 入力途中の下書き | 消えたら困るが、消える前提で「サーバーにも保存」できる |
| 一度見た案内の既読フラグ | 最悪もう一度出るだけ |
| 入れてはいけないもの | 理由 |
|---|---|
| 消えて困るデータ | 7日で消える。唯一の保存先にしない |
| 認証トークン・セッションID | JavaScriptから読める=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つあるので、選ぶ基準を表にします。
| localStorage | sessionStorage | Cookie | IndexedDB | |
|---|---|---|---|---|
| 寿命 | 明示的に消すまで(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つです。
-
文字列しか入らない。
JSON.stringify/JSON.parseを通す。パースもtry/catchで囲む。 -
「期限がない」は実態と違う。
Safariは7日で消します。IndexedDBに移しても同じです。消えて困るものはサーバーへ。 -
落ちる前提で書く。
プライベートモードと容量超過で例外が出ます。トークンは絶対に入れない。
解説記事をコピーする前に、それがいつ書かれたものかを見てください。 APIが変わっていなくても、前提のほうが変わっていることがあります。
SSRで localStorage is not defined が出る話は、原因がまったく別なので
localStorage is not defined の直し方 に分けて書きました。
Next.js で "use client" を付けても直らない理由はそちらです。
同じ型の話を PHPでOGPを取得する にも書きました。
あちらは標準APIが新しくなったのに古いライブラリが勧められ続けているケースです。
JavaScriptで要素を取る話は jQuery で自分自身の要素を取得する に。
公式情報は2026年10月25日時点で MDN と WebKit公式ブログから確認したものです。 ブラウザのストレージ方針は変わります。 重要なデータを扱う前に、各ブラウザの最新の挙動を確認してください。