- 技術 💻
localStorage・sessionStorage・Cookieの違い|実測比較
localStorage・sessionStorage・Cookieの違いは、「どれだけの期間残るか」「どの範囲で共有されるか」「サーバーへ自動で送られるか」の3点です。 localStorageはブラウザを閉じても残り同じオリジンの全タブで共有、sessionStorageはタブごとでタブを閉じると消え、Cookieは有効期限を指定でき、リクエストのたびにサーバーへ自動送信されます。
この記事では、3つの違いを容量・有効期限・共有範囲・自動送信・HttpOnly・XSS時の影響まで1枚の比較表にまとめ、Playwright 1.56 + Chromium 141での実行結果を載せます。認証トークンの置き場所の判断フローと期限つきJSON保存ユーティリティもあわせて紹介します。
localStorage・sessionStorage・Cookieの違い 早見表
| 項目 | localStorage | sessionStorage | Cookie |
|---|---|---|---|
| 容量の目安 | オリジンごと5MiB(Chromium実測:キー+値で5,242,880文字) | 同左(タブごと・別枠) | 1個約4KB、ドメインごとに個数上限 |
| 有効期限 | なし(消すまで残る) | タブを閉じるまで | Max-Age / Expires。未指定はブラウザ終了まで |
| 共有範囲 | 同じオリジンの全タブ | そのタブだけ(window.open の子には複製) |
同じドメイン・パスの全タブ |
| サーバーへの自動送信 | されない | されない | リクエストごとに自動送信 |
| JavaScriptから読み書き | できる | できる | できる(HttpOnly なら読めない) |
| 保存できる型 | 文字列 | 文字列 | 文字列 |
| セキュリティ属性 | なし | なし | HttpOnly / Secure / SameSite / Domain / Path |
| XSS(不正なスクリプト実行)時 | 全部読まれる | 全部読まれる | HttpOnly なら値は読まれない |
| 変更の通知 | 他タブに storage イベント |
なし(実測) | なし |
| 主な用途 | 表示設定、下書き、既読フラグ | 入力途中のフォーム、タブ単位の絞り込み | セッションID、サーバーが読む設定 |
迷ったら「サーバーが読む値か」→ Cookie、「タブを閉じたら消えてほしいか」→ sessionStorage、「それ以外の画面の設定」→ localStorage と考えます。大きなデータには次の2つも候補です。
| 仕組み | ひとことで |
|---|---|
| IndexedDB | ディスク容量に応じた大容量・非同期で、オブジェクトやBlobをそのまま保存できるブラウザ内データベース |
| Cache Storage | Request と Response の組を保存。Service Workerでオフライン表示に使う |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「localStorage・sessionStorage・Cookieの違い|実測比較」
基本の使い方(コピペ用)
localStorageとsessionStorageは同じAPI(Web Storage API)です。 setItem・getItem・removeItem・clear・key(i)・length で足ります。
localStorage.setItem("theme", "dark"); // ブラウザを閉じても残る
localStorage.getItem("theme"); // "dark"
localStorage.getItem("none"); // null(存在しないキー)
localStorage.removeItem("theme");
sessionStorage.setItem("step", "2"); // このタブだけ
Cookieは document.cookie に「名前=値; 属性」を代入して1つずつ書きます。読むと全Cookieが ; 区切りの1つの文字列で返ります。
document.cookie = "theme=dark; Path=/; Max-Age=2592000; SameSite=Lax; Secure";
const getCookie = (name) =>
document.cookie.split("; ").find((c) => c.startsWith(name + "="))
?.slice(name.length + 1) ?? null;
document.cookie = "theme=; Path=/; Max-Age=0"; // 削除(Pathは書き込み時と同じに)
ログインのセッションIDは、サーバーのレスポンスヘッダー Set-Cookie で発行します。 HttpOnly はサーバーからしか付けられないためです。
Set-Cookie: sid=abc123; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600
保存できるのは文字列だけで、Chromium 141で試すと次のように変換されました。
localStorage.setItem("n", 1);
localStorage.setItem("o", { a: 1 });
localStorage.setItem("u", undefined);
[typeof localStorage.getItem("n"), localStorage.getItem("n"),
localStorage.getItem("o"), localStorage.getItem("u"), localStorage.getItem("none")];
[ 'string', '1', '[object Object]', 'undefined', null ]
オブジェクトは "[object Object]" になり元に戻せないので、JSON.stringify / JSON.parse を使います(完成形は後半のユーティリティ)。
容量の上限を実測:QuotaExceededError が出るところ
MDNによると、localStorageとsessionStorageはオリジンごとにそれぞれ5MiBまでです。 何で数えるのかを、Chromiumで二分探索して確かめました。
for (const ch of ["a", "あ"]) {
localStorage.clear();
let lo = 0, hi = 20 * 1024 * 1024, err;
while (lo < hi) {
const mid = Math.ceil((lo + hi) / 2);
try { localStorage.setItem("k", ch.repeat(mid)); lo = mid; }
catch (e) { hi = mid - 1; err = `${e.name} / ${e.message}`; }
}
console.log(ch, { maxChars: lo, err });
}
実行結果(Playwright 1.56.1 / Chromium 141.0.7390.37):
a {
maxChars: 5242879,
err: "QuotaExceededError / Failed to execute 'setItem' on 'Storage': Setting the value of 'k' exceeded the quota."
}
あ {
maxChars: 5242879,
err: "QuotaExceededError / Failed to execute 'setItem' on 'Storage': Setting the value of 'k' exceeded the quota."
}
半角「a」でも全角「あ」でも同じ5,242,879文字で、キー「k」の1文字を足すと5,242,880文字=5×1024×1024です。追加で、キー5文字+値5,242,875文字は保存でき、そこへ空のキーを足すとエラー、"a"と"b"に300万文字ずつでも"b"でエラー(全キーの合計)、sessionStorageは別枠で同じだけ入りました。
Chromiumの上限は「バイト数」ではなく「キーと値の文字数(UTF-16単位)の合計」です。 日本語だから半分しか入らない、ということはありません(他ブラウザは数え方が異なる場合があります)。大きなデータはIndexedDBへ移します。
タブ・ウィンドウ間の共有を実測
同じブラウザで、タブ1で次の値を入れてから各パターンで読み出しました。
sessionStorage.setItem("draft", "下書き");
localStorage.setItem("shared", "共有");
| パターン | sessionStorage "draft" | localStorage "shared" |
|---|---|---|
| タブ1をリロード | 下書き(残る) |
共有 |
| 新しいタブで同じURLを開く | null(空) |
共有 |
タブ1から window.open(url) |
下書き(複製) |
- |
| 開いた側で変更 → タブ1で読む | 下書き のまま(別物のコピー) |
- |
window.open(url, "_blank", "noopener") |
null |
- |
| 別のブラウザプロファイル | - | null |
localhost → 127.0.0.1(別オリジン) |
- | null |
sessionStorageは「タブごと」で、新しいタブでは空です。 ただし window.open や noopener なしの target="_blank" で開いたタブには、開いた時点の内容がコピーされ、その後は独立します。MDNでは「タブを複製」でもコピーされると説明されています(UI操作のため未実測)。
localStorageは「オリジン(プロトコル+ホスト+ポート)」単位です。 localhost と 127.0.0.1 も別オリジンです。
storageイベントで別タブに変更を知らせる
localStorageを変更すると、同じオリジンの「ほかの」タブで storage イベントが発生します。 ログアウトやテーマ変更を全タブへ反映するのに使えます。
// タブ2:受け取る
addEventListener("storage", (e) => {
console.log({ key: e.key, oldValue: e.oldValue, newValue: e.newValue,
area: e.storageArea === localStorage ? "local" : "session" });
});
// タブ1:変更する
localStorage.setItem("cart", "3");
localStorage.setItem("cart", "4");
localStorage.removeItem("cart");
sessionStorage.setItem("s", "1");
実行結果(Chromium 141):
別タブ: [
{ key: 'cart', oldValue: null, newValue: '3', area: 'local' },
{ key: 'cart', oldValue: '3', newValue: '4', area: 'local' },
{ key: 'cart', oldValue: '4', newValue: null, area: 'local' }
]
変更したタブ: []
変更したタブ自身ではイベントは発生せず、sessionStorageの変更は別タブに届きませんでした。 自タブも含めて通知したいときは new BroadcastChannel("auth") の postMessage / onmessage が向いています。
Cookieの属性と、HttpOnlyがJSから見えないことを実測
Cookieだけの特徴は「サーバーへの自動送信」と「属性による制御」です。 テスト用のNode.jsサーバーで /login 時に次の3つを発行し、別ページで確認しました。
Set-Cookie: sid=abc123; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600
Set-Cookie: theme=dark; Path=/; Max-Age=60
Set-Cookie: tmp=1; Path=/
document.cookie;
document.cookie = "sid=hacked; Path=/"; // HttpOnlyの上書きを試す
document.cookie = "big=" + "a".repeat(5000) + "; Path=/"; // 約5KB
document.cookie = "ok=" + "a".repeat(4000) + "; Path=/"; // 約4KB
実行結果(有効期限はPlaywrightの context.cookies() で確認):
document.cookie: "theme=dark; tmp=1"
after write: [ 'theme=dark', 'tmp=1', 'ok=aaaaaaaaaaaaaaaaa…(4003)' ]
sid abc123 { httpOnly: true, secure: true, sameSite: 'Lax', expiresIn: '3600s' }
theme dark { httpOnly: false, secure: false, sameSite: 'Lax', expiresIn: '60s' }
tmp 1 { httpOnly: false, secure: false, sameSite: 'Lax', expiresIn: 'session' }
サーバーが受け取ったリクエストヘッダー:
GET /login Cookie: (なし)
GET /after Cookie: sid=abc123; theme=dark; tmp=1
| 確認したこと | 結果 |
|---|---|
HttpOnly の sid |
document.cookie に出ない。JSからの上書きも反映されず abc123 のまま |
HttpOnly Cookieの送信 |
リクエストには自動で付く |
Max-Age / 期限なし |
3600秒・60秒で保存/期限なしは session |
| 約5,000バイトのCookie | エラーも出ずに保存されない |
| 約4,000バイトのCookie | 保存され、以降の全リクエストに付く |
Cookieのサイズ超過はエラーにならず無視されます。 全リクエストに付くので値は小さく保ちます。なお Secure 付きが http://localhost で保存されたのは、Chromiumが localhost を安全なコンテキストとして扱うためで、本番の http:// では保存されません。
ほかに SameSite=None は Secure 必須、有効期限はChromeでは最大400日、名前が __Host- で始まるCookieは Secure・Path=/・Domain なしが強制されます。SameSite とCSRF(他サイトから勝手に操作させる攻撃)の関係は「CSRF対策とSameSite」の記事で検証しています。
認証トークンはどこに置く?判断フロー
OWASPのHTML5 Security Cheat Sheetは「セッション識別子はJavaScriptから常にアクセスできるlocalStorageに保存せず、HttpOnly 付きCookieを使う」と推奨しています。
flowchart TD
A[保存したい値] --> B{ログイン状態を表す値?}
B -- はい --> C{同じサイトのサーバーが検証する?}
C -- はい --> D[HttpOnly Secure SameSite 付きCookie]
C -- 別ドメインのAPIを直接呼ぶ --> E[アクセストークンはメモリに保持<br>更新用トークンはHttpOnly Cookie]
B -- いいえ --> F{サーバーが毎回読む?}
F -- はい --> G[小さな通常のCookie]
F -- いいえ --> H{タブを閉じたら消えてほしい?}
H -- はい --> I[sessionStorage]
H -- いいえ --> J{数MB超・構造化データ?}
J -- はい --> K[IndexedDB]
J -- いいえ --> L[localStorage]
| 置き場所 | XSSが起きたとき | CSRFへの備え |
|---|---|---|
HttpOnly Cookie |
値は読まれない(画面上から操作はされうる) | SameSite・CSRFトークンが必要 |
| メモリ(JSの変数) | 読まれうるが、リロードで消える | 自動送信されない |
| sessionStorage | 全部読まれる | 自動送信されない |
| localStorage | 全部読まれ、長く残る | 自動送信されない |
どこに置いても、XSSを防ぐことが土台です。 対策は「XSS対策」、トークンの仕組みは「JWTとは」の記事にまとめています。
期限つきJSON保存ユーティリティ(コピペ用)
localStorageには有効期限がないため、「期限」「JSON変換」「容量超過」「使えない環境」を1か所で扱う関数を用意します。 ES Modules形式で、ブラウザでそのまま使えます。
// storage-util.js
const PREFIX = "app:"; // 他の機能とキーがぶつからないよう接頭辞をつける
function getStore(area = "local") {
try {
const s = area === "session" ? sessionStorage : localStorage;
s.setItem("__t", "1");
s.removeItem("__t");
return s;
} catch {
return null; // ストレージが無効化されている環境など
}
}
export function save(key, value, { ttlSec, area } = {}) {
const s = getStore(area);
if (!s) return false;
const record = { v: value, exp: ttlSec ? Date.now() + ttlSec * 1000 : null };
try {
s.setItem(PREFIX + key, JSON.stringify(record));
return true;
} catch (e) {
if (e instanceof DOMException && e.name === "QuotaExceededError") return false;
throw e;
}
}
export function load(key, fallback = null, { area } = {}) {
const s = getStore(area);
const raw = s?.getItem(PREFIX + key);
if (raw == null) return fallback;
try {
const { v, exp } = JSON.parse(raw);
if (exp !== null && Date.now() > exp) {
s.removeItem(PREFIX + key); // 期限切れは読んだときに消す
return fallback;
}
return v;
} catch {
s.removeItem(PREFIX + key); // 壊れたJSONは消して既定値を返す
return fallback;
}
}
export function remove(key, { area } = {}) {
getStore(area)?.removeItem(PREFIX + key);
}
使い方と、Chromium 141での実行結果です(出力はコメントの番号順)。
import { save, load } from "/storage-util.js";
save("settings", { theme: "dark", pageSize: 20 }); // 期限なし
save("draft", { title: "在庫確認メモ", body: "…" }, { ttlSec: 1 }); // 1秒
save("filter", ["未完了", "自分"], { area: "session" });
load("settings"); // 1
load("filter", [], { area: "session" }); // 2
localStorage.getItem("app:draft"); // 3 保存された文字列
await new Promise((r) => setTimeout(r, 1100));
load("draft", "期限切れ"); // 4
localStorage.setItem("app:broken", "{oops");
load("broken", "既定値"); // 5
save("big", "x".repeat(6_000_000)); // 6
Object.keys(localStorage); // 7
1 {"theme":"dark","pageSize":20}
2 ["未完了","自分"]
3 {"v":{"title":"在庫確認メモ","body":"…"},"exp":1790673643137}
4 期限切れ
5 既定値
6 false
7 ["app:settings"]
期限切れ(4)と壊れたJSON(5)は削除されて既定値が返り、容量超過(6)は false で検知できます。 ブラウザはデータを消すことがあるため(MDNではSafariで7日間操作がないサイトのデータ削除を説明)、「消えている前提」で既定値を返す作りにしておくと安心です。
よくあるつまずきと直し方
| 症状 | 原因 | 直し方 |
|---|---|---|
取り出すと "[object Object]" |
オブジェクトをそのまま保存 | JSON.stringify / JSON.parse |
| 新しいタブでsessionStorageが空 | タブごとの仕組み | 共有したい値はlocalStorageへ |
QuotaExceededError |
オリジンごと5MiB(文字数) | 不要なキーを消す、IndexedDBへ |
| Cookieが保存されない | 4KB超え、SameSite=None に Secure なし |
サイズと属性を見直す |
| JSでCookieが消せない | Path / Domain 違い、HttpOnly |
同じ属性で Max-Age=0、HttpOnly はサーバーで削除 |
よくある質問
Q. localStorageとsessionStorage、どちらを使えばよいですか? A. ブラウザを閉じても残したいならlocalStorage、そのタブの作業中だけでよいならsessionStorageです。 入力途中のフォームや、タブごとに変えたい検索条件はsessionStorageが向いています。
Q. Cookieの代わりにlocalStorageでログイン状態を持ってもよいですか?
A. OWASPは、セッション識別子は HttpOnly 付きCookieに置くよう推奨しています。 同じサイトのサーバーで認証するなら HttpOnly; Secure; SameSite 付きCookieが基本です。
Q. 容量が足りないときはどうすればよいですか?
A. IndexedDBを使います。 非同期APIで上限が大きく、オブジェクトやファイル(Blob)もそのまま保存できます。navigator.storage.estimate() で使用量と上限を確認できます。
まとめ
- サーバーへ自動送信されるのはCookieだけ。Web Storageはブラウザ内だけで使う
- localStorageはオリジン全体で永続、sessionStorageはタブごと(
window.openの子タブには複製) - Chromiumの上限はキー+値で5,242,880文字。超えると
QuotaExceededError HttpOnlyCookieはdocument.cookieに出ず上書きもできないが、リクエストには付く- Cookieの4KB超えはエラーにならず無視されるので小さく保つ
- セッションIDは
HttpOnly; Secure; SameSite付きCookieに置くのが基本(OWASP) - 期限・JSON・容量超過はユーティリティにまとめ、消えている前提で既定値を返す
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「localStorage・sessionStorage・Cookieの違い|実測比較」
株式会社RJCでは、ブラウザの仕様を実際に動かして確かめ、その結果をプロジェクトの中で仲間と共有しています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。