资讯文章 资讯 · NEWS
存在用户浏览器里的那些东西:目录站的 localStorage、IndexedDB 和 Cookie 该存多少
2026-10-24 约 2,239 字 预计阅读 6 分钟 yiyunkj
存
目录站往浏览器里存东西的机会不多,但一旦存了,很容易产生一个错觉:以为存进去就一直在。
实际上浏览器端存储的默认模式叫尽力而为(best-effort),名字已经说明白了——浏览器不保证给你留着。磁盘紧张时被清掉、超过总配额时被清掉、Safari 上长时间没访问也会被清掉,而且不给你任何通知。

图片来源:网络,版权归原作者所有,侵权请联系删除
四种存储,各管一段
| 存储 | 适合放什么 | 容量 | 特点 |
|---|---|---|---|
| Cookie | 会话标识、服务端要读的状态 | 很小,每个几 KB | 每次请求都带上 |
| sessionStorage | 当前标签页的临时状态 | 数 MB 量级 | 关掉标签即清空 |
| localStorage | 少量、长期、简单的键值 | 通常 5 MB 左右 | 同步读写,会阻塞主线程 |
| IndexedDB | 成规模的结构化数据 | 大得多,按磁盘比例算 | 异步,支持索引和事务 |
选型的判断标准其实就两条:服务端要不要读,和数据量大不大。
服务端要读的只能放 Cookie,因为只有它会自动跟着请求发出去。但 Cookie 会被带到每一个请求上,塞太多会白白增加请求头体积,所以只放标识,别放内容。
数据量超过几百条、需要按字段查询的,用 IndexedDB。localStorage 看着简单,但它是同步 API,读写量大了会卡住主线程,而且只能存字符串,取出来还得自己解析一遍。
什么时候会被清掉
这是最该知道但最少人查的部分。各家规则不一样,但有几条是共通的:
- Chromium 系列:站点数据总量超过磁盘总量的一定比例(约 80%)时开始清除,按最近最少使用的顺序,一个个源清到回到限额以下。注意触发条件是总量而不是你自己的用量——你只用了 1 MB,也可能因为整体吃紧被清。
- Safari:智能跟踪防护(ITP)会在用户连续七天使用 Safari 但没有与该站点交互之后,删掉这个站点的全部脚本可写存储,包括 IndexedDB、localStorage、sessionStorage,以及 Service Worker 的注册和缓存。这里的七天是按「用了 Safari 的天数」算的,不是日历天数;点击、输入算交互,滚动不算。这个规则从 2019 年开始针对 Cookie,2020 年扩展到了所有脚本可写存储。
- 无痕 / 隐私窗口:另一套生命周期。Chrome 无痕模式下源的配额会降到很小比例,最后一个无痕窗口关闭时数据直接丢弃。Firefox 和 Safari 同样是会话结束即清。
结论很直接:浏览器端的存储只能当缓存用,不能当数据库用。任何写进去之后丢不掉就会有麻烦的东西,都不该放这儿。
想要更稳一点,可以主动申请
如果确实有数据希望浏览器优先保留,可以调用一下持久化申请:
if (navigator.storage && navigator.storage.persist) {
const ok = await navigator.storage.persist();
// ok 为 true 表示浏览器同意把该源标记为持久化
}
浏览器是否同意由它自己决定,通常依据是站点被使用得多不多、有没有加桌面之类的信号。而且它防的是配额压力下的清除,防不住 Safari 的七天规则,也防不住无痕模式关闭。
这条对目录站意义不大,知道有这回事就行。真正该做的是别把关键数据放这儿。
目录站该存什么,不该存什么
适合放浏览器里的,都是「丢了也不影响使用」的东西:
- 最近看过的条目 ID,用来在列表上打个「看过」标记。
- 搜索框的历史输入,方便下次直接点。
- 列表是宫格还是单列这类展示偏好。
- 展开/折叠状态、深色模式选择。
不该放的:
- 登录态的唯一凭证。应该放服务端下发的 Cookie,而且要带 HttpOnly。放在 localStorage 里的 token 一旦被 XSS 读到就彻底泄露。
- 统计用的用户标识。很多站图省事在前端生成一个 ID 存起来,结果 Safari 用户每七天就变成一个新用户,「新访客」比例被人为抬高。这种标识应该由服务端下发。
- 未提交的表单内容。用户填了一半的提交表单,丢了会骂人。要么及时存到服务端草稿,要么干脆不做本地暂存,别给一个看似有其实没有的承诺。
写之前先处理两件事
第一是容量兜底。写入可能失败,尤其是 IndexedDB,配额满了会抛 QuotaExceededError。所有写入都应该包在 try 里,失败时走降级:清掉最旧的数据重试一次,还不行就放弃这次缓存,别让功能挂掉。
try {
await idb.put('history', record);
} catch (e) {
if (e.name === 'QuotaExceededError') {
await idb.clear('history'); // 清掉最旧的一批再试
}
// 缓存失败不影响主流程
}
第二是版本迁移。IndexedDB 的结构只能在版本升级事件里改,一旦上线,后续加字段、改索引都要靠版本号推进。在数据库里留一个 schema 版本号是一劳永逸的做法——读出来发现版本不匹配,就直接清掉重建,比写一堆迁移分支稳。
localStorage 同样要防着旧数据:读出来先校验结构和类型,解析失败就当没有。用户在浏览器里手动改过、别的脚本写过同名的键、或者存的是半年前的老结构,这些情况都会遇得到。
别忘了这也是个人信息处理
本地存储同样是个人信息处理的范畴。最近访问、搜索历史这些东西都属于用户行为数据,最小必要原则一样适用——能用不关联到个人的方式实现,就别存可回溯的记录;确实要存的,隐私政策里要写清楚存了什么、存多久、用户怎么清。
顺带一提,用户手动清缓存这件事根本拦不住,也不该拦。设计时就当这些数据随时会消失,功能才不会在它消失之后出问题。