ブラウザストレージは、認証フロー、マルチタブ体験、スキーマ変更に耐えなければならないアプリケーションの一部になるまで、単純に見えます。難しいのは API ではありません。本当の判断は 誰がデータを必要とするか、どれくらいの期間保持すべきか、どのセキュリティ境界を越えるか です。
用語の補足:ブラウザには cookieStorage という標準 API はありません。document.cookie による従来の cookies と、新しい非同期 Cookie Store API(cookieStore 経由)があります。どちらも同じ cookies を管理しますが、Web Storage のように振る舞うものではありません。
概要
| Property | localStorage | sessionStorage | Cookies |
|---|---|---|---|
| Typical lifetime | Until explicitly cleared or evicted | Current tab's page session | Session-based or until their expiry date |
| Sent to server | No | No | Yes, on matching requests |
| JavaScript access | Yes, synchronously | Yes, synchronously | Yes, unless marked HttpOnly |
| Best suited for | Persistent, non-sensitive preferences | Temporary state that should survive reloads | Server-managed sessions and small server-readable values |
実務上の違いは、Web Storage がクライアント側に属し、cookies が HTTP リクエストのライフサイクルに参加する点です。どちらも信頼できる database として扱うべきではありません。
localStorage
localStorage は、ページのリロードやブラウザの再起動をまたいで、origin ごとにデータを永続化します。テーマ、非表示にした通知、未完了のローカル下書きなど、少量の非機密な preferences に向いています。
const preferences = {
theme: "dark",
density: "compact",
}
localStorage.setItem("preferences:v1", JSON.stringify(preferences))
const stored = localStorage.getItem("preferences:v1")
const parsed = stored ? JSON.parse(stored) : null便利さには限界があります。値は文字列、access は同期的で、read や write のたびに main thread をブロックします。容量や eviction の挙動もブラウザによって異なります。database ではなく、小さな persistence 機構として扱っています。
その origin 上で動く JavaScript はすべて読み取れます。XSS 脆弱性経由で混入した悪意あるコードも含まれます。そのため session tokens、passwords、機微な個人データは localStorage に保存しません。長寿命の bearer tokens は特に危険で、値を盗むだけで他の場所で再利用できます。
stored keys に version を付け、parsed data を検証します。デプロイ済みのアプリは変わりますが、古い browser data は残ります:
type Preferences = {
theme: "light" | "dark"
}
function readPreferences(): Preferences | null {
try {
const value = JSON.parse(localStorage.getItem("preferences:v1") ?? "null")
if (value?.theme === "light" || value?.theme === "dark") {
return value
}
} catch {
// Corrupt or manually edited storage should not break the application.
}
return null
}sessionStorage
sessionStorage は似た文字列ベースの API ですが、lifetime は page session に紐づきます。同一タブ内の reload には耐え、通常はそのタブや window を閉じると消えます。タブごとに storage は独立しています。
そのため、一時的な UI state に向いています。多段階フォーム、return URL、誤った refresh には耐えるが永続 preference にはならない filters などです。
sessionStorage.setItem(
"checkout:draft",
JSON.stringify({ step: 2, deliveryMethod: "pickup" })
)セキュリティ境界ではありません。ページ上の scripts からも読めますし、ユーザーがタブを複製・復元する方法によって lifecycle の前提が崩れることもあります。sessionStorage を使う理由は product lifetime がデータと一致するからであり、localStorage より安全だからではありません。
Cookies と Cookie Store API
Cookies は、ブラウザが HTTP リクエストに付与できる点が異なります。サーバーが値を必要とする場合、特に server-managed sessions に適しています。
セキュリティ上重要な cookies は、通常サーバー側で作成すべきです:
Set-Cookie: session=opaque-value; Path=/; HttpOnly; Secure; SameSite=LaxHttpOnly は JavaScript による cookie の読み取りを防ぎ、Secure は HTTPS に限定し、SameSite は cross-site request forgery を抑えるのに役立ちます。これらの attributes はリスクを下げますが、output escaping、Content Security Policy、CSRF 分析、適切な session expiration と rotation の代わりにはなりません。
従来の document.cookie は同期的で、パースも扱いにくいです。Cookie Store API は、対応環境では非同期 interface を提供します:
const preference = await cookieStore.get("theme")
await cookieStore.set({
name: "theme",
value: "dark",
path: "/",
sameSite: "lax",
})Client-side code は cookieStore 経由でも HttpOnly cookie を読めません。その制限が意図です。一般的な application state を cookies に入れることも避けます。サイズが小さく、domain と path のルールが微妙で、リクエストに含まれる cookies は毎回ネットワーク overhead を増やします。
私が使う判断基準
デフォルトの選択は次のとおりです:
- データが現在開いているページの間だけ必要なら in-memory state
- 一時 state が単一タブ内の reload に耐えるべきなら sessionStorage
- 訪問をまたいで残る少量の非機密 preferences なら localStorage
- サーバーが値を受け取る必要がある場合、特に secure な HTTP-only cookie 内の opaque session identifier なら cookies
- 大量データ、structured records、transactions、offline 動作が必要なら IndexedDB
原則は、最短の有用 lifetime で、最小限のデータを保存することです。Browser storage はユーザーが制御でき、消去・改変され得ます。authoritative な source of truth として扱うべきではありません。読み取り時に values を検証し、storage failures を処理し、server-side authorization はクライアントが編集できるものに依存させないでください。
ストレージの選択は、最終的には API の好みではなく architecture の判断です。データの lifetime と trust level から始めれば、適切な browser primitive はたいてい自然についてきます。