做前端久了你会发现,几乎每个项目都绕不开浏览器端存储:登录态要留、用户偏好要记、离线缓存要做、表单草稿别丢。但 localStorage、sessionStorage、Cookie、IndexedDB 到底该用谁?选型错了,轻则刷新丢数据,重则遭遇存储型 XSS、写下阻塞主线程的同步代码。本文用一张对比表加两个可直接抄的封装,把IndexedDB 与 localStorage 的边界、实战与选型讲透。
一、四种存储方案一览
浏览器原生提供了四种主要存储能力,它们的容量、生命周期和读写方式差异巨大:
| 方案 | 容量 | 读写 | 生命周期 | 典型用途 |
|---|---|---|---|---|
| localStorage | 约 5MB | 同步 | 永久(手动清) | 用户偏好、Token、小配置 |
| sessionStorage | 约 5MB | 同步 | 标签页关闭即清 | 单会话临时状态 |
| Cookie | 约 4KB | 同步 | 可设过期 | 鉴权凭证、服务端可读 |
| IndexedDB | 数百 MB+ | 异步 | 永久(手动清) | 结构化数据、离线库、大对象 |
可以看到,localStorage 适合小、简单、同步读写的键值;而一旦你要存结构化对象、做离线数据库或缓存大文件,就该上 IndexedDB。关于网络层缓存,推荐延伸阅读 前端缓存策略实战:强缓存、协商缓存与 Service Worker,它解决的是「请求结果复用」,与本文的「应用数据落地」互补。
二、localStorage:最简单的关键值存储
localStorage 的 API 只有 getItem / setItem / removeItem / clear 四个方法,所有值都以字符串存储,且同源可读、永久保留。
2.1 它的问题:没有过期、只能存字符串
原生 API 不支持过期时间,存对象还要手动 JSON.stringify。一个健壮的做法是封装一层,自动加过期时间:
// 带 TTL 的 localStorage 封装
function setLSCache(key, value, ttlSeconds) {
const payload = {
v: value,
exp: ttlSeconds ? Date.now() + ttlSeconds * 1000 : 0,
};
localStorage.setItem(key, JSON.stringify(payload));
}
function getLSCache(key) {
const raw = localStorage.getItem(key);
if (!raw) return null;
try {
const { v, exp } = JSON.parse(raw);
if (exp && Date.now() > exp) {
localStorage.removeItem(key);
return null;
}
return v;
} catch {
return null; // 解析失败直接丢弃,避免脏数据
}
}
// 用法:缓存用户偏好 7 天
setLSCache("theme", "dark", 7 * 24 * 3600);
const theme = getLSCache("theme");
2.2 避坑:同步读写会阻塞主线程
localStorage 是同步的,一次写入会锁住主线程。虽然 5MB 很小,但当你在循环里频繁写入(例如埋点、滚动监听里写坐标),就可能造成可感知的卡顿。高频写入请节流,或改用 IndexedDB。更多首屏与性能细节可看 前端性能优化:Lighthouse 90+ 到首屏 1s。
三、sessionStorage 与 Cookie 的边界
sessionStorage 与 localStorage API 完全一致,区别在于生命周期随标签页:关掉页面数据就清空,适合「单会话临时态」,比如多步表单的进度、一次性验证码状态。
Cookie 则是一枚老兵:它每次请求自动携带,服务端可读,因此天然适合放鉴权凭证。但容量只有约 4KB,且必须通过 Secure / HttpOnly / SameSite 等属性加固。关于 HttpOnly 如何挡住脚本读取、SameSite 如何防 CSRF,推荐延伸阅读 前端安全实战:XSS 与 CSRF 防御全指南。
四、IndexedDB:浏览器里的结构化数据库
当你需要存用户草稿、离线消息、长列表缓存、甚至图片二进制时,localStorage 的 5MB 和字符串限制就不够了。IndexedDB 是一个异步、事务化、支持索引的 NoSQL 数据库,容量可达数百 MB 甚至更多。
4.1 核心概念
理解三个概念即可上手:
- 数据库(DB):通过版本号创建与升级;
- 对象仓库(objectStore):类似一张表,存放键值对象;
- 索引(index):在对象字段上建索引,支持按非主键查询。
4.2 实战:Promise 化封装
原生 IndexedDB 回调嵌套很深,工程里建议先包一层 Promise。下面这个封装覆盖「开库 / 写 / 读 / 删 / 按索引查」:
function openDB(name, version, upgrade) {
return new Promise((resolve, reject) => {
const req = indexedDB.open(name, version);
req.onupgradeneeded = () => upgrade(req.result);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
function tx(db, store, mode, fn) {
return new Promise((resolve, reject) => {
const t = db.transaction(store, mode);
const os = t.objectStore(store);
const res = fn(os);
t.oncomplete = () => resolve(res && res.result);
t.onerror = () => reject(t.error);
});
}
// 开库并在 user 仓库上建 email 索引
const db = await openDB("app", 1, (db) => {
const os = db.createObjectStore("user", { keyPath: "id" });
os.createIndex("email", "email", { unique: false });
});
// 写
await tx(db, "user", "readwrite", (os) =>
os.put({ id: 1, name: "Aki", email: "aki@example.com" })
);
// 按主键读
const u = await tx(db, "user", "readonly", (os) => os.get(1));
// 按索引查
await tx(db, "user", "readonly", (os) =>
os.index("email").get("aki@example.com")
);
4.3 存二进制:Blob、File、ArrayBuffer 直接进库
IndexedDB 支持结构化克隆,能直接存 Blob / File / ArrayBuffer,无需 base64 转字符串。这意味着你可以把用户头像、离线 HTML、导出的 JSON 原样落地:
// 把一张图片 Blob 存进 assets 仓库
await tx(db, "assets", "readwrite", (os) =>
os.put({ id: "avatar-1", blob: fileBlob, type: fileBlob.type })
);
// 读取后通过 URL.createObjectURL 直接渲染
const asset = await tx(db, "assets", "readonly", (os) => os.get("avatar-1"));
const url = URL.createObjectURL(asset.blob);
五、选型决策矩阵
面对具体需求,用下面这张表快速决策:
| 你的需求 | 首选方案 | 理由 |
|---|---|---|
| 记住主题 / 语言偏好 | localStorage | 小、永久、同步即可 |
| 多步表单临时态 | sessionStorage | 关页即清,不污染全局 |
| 鉴权 Token(服务端校验) | Cookie | 自动携带、可 HttpOnly |
| 离线草稿 / 长列表缓存 | IndexedDB | 大容量、结构化、异步 |
| 缓存头像 / 文件二进制 | IndexedDB | 原生支持 Blob |
六、三个高频踩坑
6.1 隐私模式写入会抛异常
Safari 无痕模式、部分浏览器的隐私模式下,localStorage / IndexedDB 写入会直接抛错或静默失败。所有写操作务必 try/catch,并准备降级(如内存 Map)。
6.2 存储型 XSS 借 localStorage 长期潜伏
localStorage 里的数据会在页面上下文被 JS 读取,若你直接把其中内容 innerHTML 到页面,攻击者注入的脚本就会被持久执行。永远不要信任存储里的内容,输出到 DOM 前做转义。更多加固手段见 前端安全实战:XSS 与 CSRF 防御全指南。
6.3 别把大对象塞进 localStorage
超过 5MB 或需要频繁更新的数据,请直接上 IndexedDB。把整页状态序列化进 localStorage 是常见的性能反模式。
七、落地建议:组合使用
真实项目往往是组合拳:用 Cookie 存鉴权凭证,用 localStorage 存用户偏好,用 IndexedDB 做离线数据与草稿,再叠一层 Service Worker 网络缓存 提速请求。单页应用里,路由切换时想保持登录态,可参考 前端路由原理与实战:Hash 与 History 模式选型 中关于状态保持的写法;若把存储封装放进 Vue3 + TypeScript 工程化 的统一工具层,还能顺便获得类型安全。
八、容量、配额与清理策略
浏览器存储并非无限。IndexedDB 的可用空间由各浏览器的「站点存储配额」决定,通常只占磁盘的一小部分,且不同源之间互不干扰。你可以用 navigator.storage.estimate() 在运行时读取已用空间与配额上限,实现「空间不足时主动清理最旧缓存」的自我保护,而不是等到写入抛错才处理。
另一个常被忽视的点是清理时机。localStorage 不会自动过期,长期堆积会逐渐拖慢首次读取;IndexedDB 的草稿表如果不设上限,也可能在低端机上占满配额,导致后续写入静默失败。建议给每个仓库设定最大条数或最大保留时间,超过阈值就按时间顺序淘汰。对于离线场景,还可以在「仅 WiFi 且空闲」时做一次全量瘦身。
最后,涉及用户隐私的数据(如定位轨迹、聊天记录、表单草稿)在用户注销或撤回同意时应主动删除,这既是体验问题,也是合规底线。存储方案选型时把「能否被干净清除」也纳入考量,会让你的前端在审计时少很多麻烦。
小结
浏览器端存储没有「最好」的方案,只有「最合适」的选型:localStorage 管小偏好、sessionStorage 管临时态、Cookie 管凭证、IndexedDB 管结构化与离线。先想清楚容量、生命周期和是否异步,再动手封装一层带过期与容错的薄壳,就能避开绝大多数坑。把这篇文章的封装抄进项目,你的存储层就稳了一大半。




