K 的一隅

浏览器 浏览器核心机制

数据明明只想存一会儿,为什么刷新后还在:浏览器存储

从临时便签、会员卡和仓库档案的不同柜子说起,理解 Cookie、Web Storage、IndexedDB 与 Cache Storage 的生命周期。

5 分钟阅读 更新于 2026-07-24
本文目录

临时便签、会员卡和仓库档案不会放在同一个柜子里。便签拿来记一个短暂状态,会员卡会在每次进店时被出示,档案则需要容量、索引和异步取用。浏览器的 Cookie、localStorage、sessionStorage、IndexedDB 和 Cache Storage 也有不同的所有者、容量和生命周期。

先猜:刷新后谁还记得

“能不能记住”取决于存储介质的生命周期和它是否参与网络请求。

js
localStorage.setItem('theme', 'dark')
sessionStorage.setItem('draft-step', '2')
document.cookie = 'visit=1; Path=/'

刷新后,这三份数据都可能还在;关闭标签页后,sessionStorage 通常随页面会话消失;Cookie 还可能被后续请求自动携带;localStorage 仍留在同源存储中。这里没有一个“浏览器存储”单独开关,关键是你选择了哪个柜子。

Cookie 最特别的地方不是能存字符串,而是浏览器可能根据域、路径、安全属性和请求上下文自动发送它:

http
Set-Cookie: session=abc; Secure; HttpOnly; SameSite=Lax; Path=/

HttpOnly 让 JavaScript 不能读取这个 Cookie;Secure 要求 HTTPS;SameSite 影响跨站请求时是否携带。Cookie 适合会话标识、需要随请求到达服务器的少量数据,不适合塞进一大段页面配置。

Cookie 的自动携带也意味着它参与请求安全设计。前端能读到的 localStorage token 和浏览器自动携带的 Cookie 各有 XSS、CSRF 和过期风险,不能简单说一种永远更安全。选择应由认证流程、威胁模型和服务器契约决定。

localStorage 和 sessionStorage 是同步柜子

js
localStorage.setItem('sidebar', 'collapsed')
const value = localStorage.getItem('sidebar')
sessionStorage.setItem('checkout-step', 'payment')

Web Storage API 简单,但读写是同步调用。字符串化和解析大对象、频繁在主线程读写,可能阻塞输入和绘制。它适合少量偏好和短状态,不适合大数据、复杂索引或高频写入。

localStorage 通常按源持久保存;sessionStorage 通常绑定到标签页会话,但具体行为还受窗口复制、隐私模式和浏览器策略影响。容量不是业务可以无限依赖的配额,用户清理站点数据、浏览器回收空间或隐私设置都可能改变结果。

“只在本地”也不代表安全。任何能执行同源脚本的 XSS 都可能读取 localStorage。不要把密码、长期秘密或高价值凭证随意放在那里。

IndexedDB 解决的是另一类问题

IndexedDB 是异步的结构化数据存储,适合较大数据、索引、离线队列和需要事务边界的场景:

js
const request = indexedDB.open('notes', 1)
request.onupgradeneeded = () => {
  request.result.createObjectStore('items', { keyPath: 'id' })
}
request.onsuccess = () => {
  const db = request.result
  const tx = db.transaction('items', 'readwrite')
  tx.objectStore('items').put({ id: 1, text: '离线笔记' })
}

它不是自动过期的数据库。你需要设计 schema、版本升级、失败处理、清理策略和数据迁移。事务让一组操作有一致性边界,但不会替你解决多个标签页同时更新、服务器冲突和用户清除站点数据。

IndexedDB 适合离线编辑器、图片元数据和较大缓存;如果只是记住主题,使用 localStorage 更直接。选择存储时,先看数据大小、访问频率、是否需要索引、是否需要随请求上传,而不是先问“哪个 API 最现代”。

Cache Storage 不只是另一种 localStorage

Service Worker 常用 Cache Storage 保存完整的 Request/Response:

js
const cache = await caches.open('assets-v1')
await cache.put('/offline.html', new Response('<h1>离线</h1>'))
const response = await cache.match('/offline.html')

它适合离线资源和 HTTP 响应,不适合当作任意 JSON 数据库。请求方法、URL、响应头和缓存版本都会影响匹配。资源缓存和业务状态混用,更新时很容易出现“页面壳是新版,数据是旧版”的组合。

Cache Storage、HTTP cache、IndexedDB 彼此独立。清理一个,不等于清理其他两个。Application 面板里要逐个看清楚来源。

生命周期要写进数据模型

一份数据可能希望活到本次组件卸载、本次标签页关闭、下次访问、登录会话结束、服务器明确过期,或者直到用户手动清理。存储 API 不会自动知道你的业务意图。

ts
type Draft = {
  text: string
  savedAt: number
  schemaVersion: 2
}

localStorage.setItem('draft', JSON.stringify(draft))

写入时间和 schemaVersion(结构版本)是很小但有用的字段。读取时检查版本、过期时间和解析失败;不要把“读取成功”当成“数据仍然可用”。旧版本应用、半写入数据、用户手动改值都可能让 JSON 结构不符合当前代码。

跨标签页更新可以通过 storage 事件、BroadcastChannel 或服务器同步通知;但这些机制也有各自的延迟和边界。多个标签页同时改草稿时,最后写入不一定是用户真正想保留的版本。

容易踩的坑

localStorage 同步读写、容量有限,不适合塞整站状态;Cookie 也不是它的旧版——Cookie 会随请求走,还有 HttpOnly、SameSite 等与服务器协作的属性。IndexedDB 写入成功不保证永久:配额、隐私模式和清理策略都能让数据消失。Cache Storage 面向 Request/Response,不能当成带索引事务的通用数据库来用。