"这个数据存哪儿?"是前端的日常问题。存 Cookie?localStorage?还是上 IndexedDB?选错了,轻则容量不够、性能拉胯,重则把 token 暴露在 XSS 之下。
这篇把浏览器端几种主要存储方案放在一起,讲清楚各自的边界和该用在哪。
先看全景对比
| 方案 | 容量 | 生命周期 | 是否随请求发送 | 可被 JS 读取 | 数据结构 |
|---|---|---|---|---|---|
| Cookie | ~4KB | 可设过期时间 | 是(每次请求自动带) | 取决于 HttpOnly | 字符串 |
| localStorage | ~5–10MB | 永久(手动清除) | 否 | 是 | 字符串 |
| sessionStorage | ~5–10MB | 标签页关闭即清 | 否 | 是 | 字符串 |
| IndexedDB | 通常数百 MB 起 | 永久 | 否 | 是 | 结构化对象 |
| Cache Storage | 受配额限制 | 永久 | 否 | 是 | Request/Response |
下面逐个说它们的关键特性和适用场景。
Cookie:为"跟服务器交互"而生
Cookie 最大的特点是:它会在每次同源请求时自动加到请求头里发给服务器。这既是它的用途,也是它的负担。
// 设置(注意:这是不安全的客户端写法,仅演示)
document.cookie = 'theme=dark; max-age=2592000; path=/; SameSite=Lax'
// 读取(原生 API 很难用,只能拿到一整个字符串)
console.log(document.cookie) // "theme=dark; other=value"关键点:容量极小(约 4KB),且每个请求都会携带。 所以别往 Cookie 里塞大数据——每加 1KB,你的每个请求都多 1KB 流量。
Cookie 的几个安全相关属性非常重要:
HttpOnly:设了之后 JS 读不到(document.cookie拿不到),能有效防止 XSS 窃取。存 token / session 必须加。Secure:只在 HTTPS 下发送。SameSite:控制跨站请求是否携带,Lax/Strict能缓解 CSRF。
结论:Cookie 适合存需要服务器读取的、小体积的身份凭证(session id / token),而且这类凭证应该由服务端通过 Set-Cookie 带 HttpOnly 下发,前端不要用 JS 存敏感凭证。
localStorage:简单的键值持久化
localStorage 是最常用的客户端存储:同步 API、键值对、容量 5–10MB、永久保存(除非手动清除)。
localStorage.setItem('theme', 'dark')
localStorage.getItem('theme') // 'dark'
localStorage.removeItem('theme')
localStorage.clear()
// 只能存字符串,对象要序列化
localStorage.setItem('user', JSON.stringify({ name: 'Tom' }))
const user = JSON.parse(localStorage.getItem('user') || '{}')适合存:用户偏好(主题、语言)、非敏感的表单草稿、一些不重要的缓存。
要注意两点:
- 它是同步的,会阻塞主线程。存大量数据或频繁读写会影响性能。
- 只能存字符串,对象要
JSON.stringify/JSON.parse,且无法存 Date、Map 等(同 JSON 的局限)。
还有一个实用特性:storage 事件。同源的其它标签页修改 localStorage 时,当前页会收到事件,可用于多标签页同步(比如一个标签退出登录,其它标签也跟着退):
window.addEventListener('storage', (e) => {
if (e.key === 'token' && !e.newValue) {
// 另一个标签页清除了 token,这里同步登出
location.reload()
}
})注意:storage 事件只在其它标签页触发,自己改自己是收不到的。
sessionStorage:只活在当前标签页
API 和 localStorage 一模一样,唯一区别是生命周期:数据只在当前标签页有效,关闭标签页就清除。
sessionStorage.setItem('step', '2')它的作用域是"标签页级"的:
- 同一个标签页内刷新、跳转,数据还在。
- 新开一个标签页(即使同源),是独立的、空的 sessionStorage。
- 关闭标签页,数据消失。
适合存:多步表单的中间状态、一次性的页面间传参、"本次会话"才有意义的临时数据。需要"换个标签页就重来"的场景,它比 localStorage 更合适。
IndexedDB:浏览器里的数据库
当数据量大、结构复杂、需要查询时,localStorage 就不够了。IndexedDB 是浏览器内置的事务型、异步、能存结构化对象的数据库。
它的特点:
- 容量大:通常几百 MB 起步,按可用磁盘动态分配。
- 异步:不阻塞主线程,适合大数据。
- 存结构化数据:能直接存对象、数组、Blob、File,不用手动序列化成字符串。
- 支持索引和事务:能按字段查询。
原生 API 偏底层、基于事件,写起来啰嗦:
const req = indexedDB.open('myDB', 1)
req.onupgradeneeded = (e) => {
const db = e.target.result
db.createObjectStore('posts', { keyPath: 'id' })
}
req.onsuccess = (e) => {
const db = e.target.result
const tx = db.transaction('posts', 'readwrite')
tx.objectStore('posts').put({ id: 1, title: '一篇文章' })
}实战里基本不会直接用原生 API,而是用封装库(比如 idb、localforage)。localforage 甚至提供和 localStorage 几乎一样的 API,底层自动用 IndexedDB:
import localforage from 'localforage'
await localforage.setItem('user', { name: 'Tom' }) // 直接存对象,异步
const user = await localforage.getItem('user')适合存:离线数据、大量列表缓存、富文本编辑器内容、PWA 离线资源等。
Cache Storage:配合 Service Worker
Cache Storage 专门用来缓存网络请求的响应(Request/Response 对),是 PWA 离线能力的基础,一般配合 Service Worker 使用:
const cache = await caches.open('v1')
await cache.add('/index.html')
const res = await cache.match('/index.html')它不是用来存普通业务数据的,而是缓存静态资源和接口响应,实现离线访问。日常业务存储用不到它,做 PWA 时才需要。
一张选型决策表
| 你的需求 | 推荐方案 |
|---|---|
| 身份凭证 / session(要给服务器读) | Cookie(HttpOnly + Secure + SameSite) |
| 主题、语言等用户偏好 | localStorage |
| 多步表单、仅本标签页有效的临时数据 | sessionStorage |
| 大量结构化数据、离线缓存、需要查询 | IndexedDB(用 idb / localforage 封装) |
| 缓存请求响应、PWA 离线 | Cache Storage + Service Worker |
安全与常见误区
第一,别把 token 存 localStorage。 这是最常见的错误。localStorage 能被任意 JS 读取,一旦站点有 XSS,token 就被偷了。敏感凭证应放在 HttpOnly Cookie 里,JS 碰不到。
第二,localStorage 不是缓存层,没有过期机制。 它不会自动过期,要"带过期时间"得自己存一个时间戳,读取时判断:
function setWithTTL(key, value, ttlMs) {
localStorage.setItem(key, JSON.stringify({ value, expire: Date.now() + ttlMs }))
}
function getWithTTL(key) {
const raw = localStorage.getItem(key)
if (!raw) return null
const { value, expire } = JSON.parse(raw)
if (Date.now() > expire) {
localStorage.removeItem(key)
return null
}
return value
}第三,别在 localStorage 里存大数据。 它是同步的,存几 MB 的 JSON 会卡主线程。大数据用 IndexedDB。
第四,存储都是按源(origin)隔离的。 http 和 https、不同端口、不同域名,存储互不相通。调试时别把这点忘了。
第五,容量会满,要处理写入失败。 localStorage 写满会抛 QuotaExceededError,关键写入要 try/catch。
总结
浏览器存储选型,核心就两个维度:数据要不要给服务器 和 数据有多大/多复杂。
- 要给服务器、且是凭证 → Cookie(带
HttpOnly)。 - 小、纯客户端、要持久 → localStorage。
- 小、纯客户端、只活在本标签页 → sessionStorage。
- 大、结构化、要查询或离线 → IndexedDB。
记牢一条安全红线:敏感凭证只进 HttpOnly Cookie,永远别进 localStorage。把这些理清楚,"数据存哪儿"就不再是拍脑袋的决定了。