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
  • HttpOnly Cookieは 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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?