本地存储方案对比

16 分钟

背景:为什么要关注浏览器存储

HTTP 协议是无状态的——服务器不会记住上一次请求是谁发的。为了在客户端保留用户身份、缓存数据、维持交互状态,浏览器提供了多种存储机制。面试中经常被问到的就是:cookie、localStorage、sessionStorage、IndexedDB 四者有什么区别,各自适合什么场景。

这篇文章按照"逐个拆解 → 横向对比 → 选型建议"的思路展开,覆盖面试高频追问点。

本质与用途

Cookie 最初被设计用来解决 HTTP 无状态问题。服务器通过 Set-Cookie 响应头将数据写入浏览器,浏览器在后续同源请求中自动通过 Cookie 请求头携带这些数据。

核心特征:每次同源 HTTP 请求都会自动携带,这既是它的能力,也是它的负担。

关键属性字段

Set-Cookie: token=abc123; Domain=.example.com; Path=/; Expires=Thu, 01 Jan 2027 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax

逐个拆解:

属性作用说明
Domain指定 cookie 可被发送到哪些域设置为 .example.com 则子域名也能读取
Path限制 cookie 在哪些路径下可用默认为当前路径
Expires / Max-Age控制过期时间不设置则为会话级 cookie,关闭浏览器即失效
HttpOnly禁止 JavaScript 通过 document.cookie 读取防御 XSS 窃取 cookie
Secure仅在 HTTPS 连接下发送防止中间人截获明文 cookie
SameSite控制跨站请求是否携带 cookieStrict 完全不带;Lax GET 导航会带;None 都带(需配合 Secure)

SameSite 深入

这是面试高频追问点。Chrome 80+ 将 SameSite 默认值从 None 改为 Lax,直接影响了跨站场景:

  • Strict:任何跨站请求都不带 cookie。从第三方链接跳转过来也不带,用户体验差(登录态丢失)。
  • Lax:跨站的 GET 导航(链接跳转、预加载)会带,但 POST 表单、iframe、AJAX 不带。适合大多数场景。
  • None:跨站也带,但必须同时设置 Secure。用于需要跨站传 cookie 的场景,如第三方登录、嵌入式 widget。

容量限制

  • 单个 cookie 大小上限约 4KB
  • 每个域名下 cookie 数量上限通常为 50 个(各浏览器略有差异)
  • cookie 会随每次请求发送,体积过大直接影响网络性能

操作 API

// 写入
document.cookie = 'name=value; max-age=3600; path=/';

// 读取(只能拿到 key=value,拿不到属性)
console.log(document.cookie); // "name=value; other=xxx"

// 删除(设置过期时间为过去)
document.cookie = 'name=; max-age=0';

API 设计非常原始——没有直接的 get/set/delete 方法,读取时得自己解析字符串。这也是为什么现代前端存储更倾向使用 Web Storage API。

localStorage

核心特性

localStorage 提供了简单的键值对持久化存储:

  • 持久化:数据不会过期,除非手动清除或用户清理浏览器数据
  • 同源共享:同一源(协议 + 域名 + 端口)下的所有标签页、窗口共享同一份数据
  • 容量:通常为 5MB(不同浏览器略有差异,部分移动端浏览器为 2.5MB)
  • 同步 API:操作会阻塞主线程

API 使用

// 写入
localStorage.setItem('user', JSON.stringify({ name: '南祎', role: 'dev' }));

// 读取
const user = JSON.parse(localStorage.getItem('user'));

// 删除单个
localStorage.removeItem('user');

// 清空当前源下所有数据
localStorage.clear();

// 获取存储的键值对数量
console.log(localStorage.length);

注意事项

  1. 只能存字符串:存对象需要 JSON.stringify,取出需要 JSON.parse。存储 undefined 会变成字符串 "undefined"
  2. 同步阻塞:大量数据读写可能阻塞渲染,不适合存储大数据。
  3. 无过期机制:需要自行实现过期逻辑(存入时带时间戳,读取时判断)。
  4. storage 事件:同源的其他标签页可以监听 storage 事件,实现跨标签页通信。
// 标签页 A 写入
localStorage.setItem('theme', 'dark');

// 标签页 B 监听变化
window.addEventListener('storage', (event) => {
  console.log(event.key);      // 'theme'
  console.log(event.oldValue); // null 或旧值
  console.log(event.newValue); // 'dark'
});

这个特性常被用作简易的跨标签页通信方案。

sessionStorage

核心特性

sessionStorage 的 API 与 localStorage 完全一致,区别在于生命周期和作用域

  • 会话级别:关闭标签页/窗口后数据清除
  • 标签页隔离:即使同源,不同标签页之间也不共享数据
  • 刷新保留:页面刷新不会丢失数据(区别于内存变量)
  • 容量:同样约 5MB

标签页隔离的细节

这是面试区分度的关键点:

// 标签页 A
sessionStorage.setItem('step', '2');

// 标签页 B(即使同源)
sessionStorage.getItem('step'); // null —— 读不到 A 的数据

但有一个特殊情况:通过 window.open<a target="_blank"> 打开的新标签页,会复制当前标签页的 sessionStorage 作为初始值,之后两者独立互不影响。

典型使用场景

  • 表单多步骤填写:防止用户刷新丢失已填内容,但关闭后不需要保留
  • 单次会话的临时状态:如当前滚动位置、展开/折叠状态
  • 防止同一用户在多个标签页重复提交

IndexedDB

为什么需要 IndexedDB

当存储需求超出简单键值对的范围——需要存储大量结构化数据、支持索引查询、处理二进制文件(图片、音频)——localStorage 的 5MB 限制和纯字符串存储就力不从心了。

IndexedDB 是浏览器内置的事务型数据库,可以理解为浏览器里的 NoSQL 数据库。

核心概念

  • Database:一个源可以有多个数据库,通过名称区分
  • Object Store:类似关系数据库的"表",存储 JavaScript 对象
  • Key:每条记录的唯一标识,可以是 keyPath(对象内某个字段)或自增 key
  • Index:在非主键字段上建索引,加速查询
  • Transaction:所有读写操作必须在事务中进行,保证数据一致性
  • Cursor:遍历大量数据的迭代器

基本操作示例

// 打开/创建数据库
const request = indexedDB.open('MyApp', 1);

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  // 创建 Object Store,指定主键
  const store = db.createObjectStore('users', { keyPath: 'id' });
  // 创建索引
  store.createIndex('nameIndex', 'name', { unique: false });
};

request.onsuccess = (event) => {
  const db = event.target.result;

  // 写入数据
  const transaction = db.transaction('users', 'readwrite');
  const store = transaction.objectStore('users');
  store.add({ id: 1, name: '南祎', age: 28 });

  // 读取数据
  const getRequest = store.get(1);
  getRequest.onsuccess = () => {
    console.log(getRequest.result); // { id: 1, name: '南祎', age: 28 }
  };

  // 通过索引查询
  const index = store.index('nameIndex');
  const indexRequest = index.get('南祎');
  indexRequest.onsuccess = () => {
    console.log(indexRequest.result);
  };
};

关键特性

特性说明
容量通常为磁盘可用空间的 50%,远超其他方案
数据类型支持结构化克隆算法能处理的所有类型(对象、数组、Blob、File、ArrayBuffer 等)
异步 API不阻塞主线程(基于事件回调,也可用 Promise 封装)
事务支持读写操作在事务内执行,保证一致性
同源策略遵循同源限制
Web Worker可在 Worker 中访问

实际使用建议

原生 API 偏底层,回调嵌套多。生产中通常使用封装库:

  • idb:轻量 Promise 封装,API 简洁
  • Dexie.js:功能丰富,支持链式查询、版本管理
  • localForage:统一的 localStorage 风格 API,底层自动选择 IndexedDB / WebSQL / localStorage

四种方案对比

维度CookielocalStoragesessionStorageIndexedDB
容量~4KB/个~5MB~5MB数百MB甚至更多
生命周期可设置过期时间永久(手动清除)标签页关闭即清除永久(手动清除)
作用域同源 + Path + Domain 控制同源所有标签页共享仅当前标签页同源所有标签页共享
是否随请求发送✅ 自动携带
API 风格字符串拼接,原始同步,简单键值对同步,简单键值对异步,事务型
数据类型字符串字符串字符串结构化数据、二进制
Web Worker 访问
典型用途身份认证、会话管理用户偏好、缓存数据临时表单状态离线应用、大量结构化数据

存储方案选型建议

选型的核心思路是根据数据特征匹配存储能力

  1. 需要服务端识别身份 → Cookie(配合 HttpOnly + Secure + SameSite)
  2. 少量配置数据,需要跨标签页持久化 → localStorage(如主题偏好、语言设置、token 缓存)
  3. 单次会话临时数据,标签页之间互不干扰 → sessionStorage(如表单草稿、支付流程状态)
  4. 大量结构化数据或离线场景 → IndexedDB(如离线邮件、本地数据缓存、大文件暂存)

一个常见的组合方案:

认证 token → Cookie(HttpOnly,防 XSS 读取)
用户偏好设置 → localStorage(持久化,跨标签页同步)
表单填写进度 → sessionStorage(标签页关闭即弃)
离线数据/缓存 → IndexedDB(容量大,支持索引查询)

安全注意事项

XSS 防护

  • Cookie 设置 HttpOnly 后,JavaScript 无法读取,降低被 XSS 窃取的风险
  • localStorage / sessionStorage 没有类似机制——一旦页面被 XSS 注入,存储的所有数据都可以被读取
  • 敏感信息(token、密钥)不应存在 localStorage 中,优先使用 HttpOnly Cookie

CSRF 防护

  • Cookie 会自动携带,天然面临 CSRF 攻击风险
  • SameSite=Lax/Strict 是目前最有效的防御手段
  • localStorage 中的 token 需要手动通过 Header 发送,天然免疫 CSRF

数据加密

  • 浏览器存储的数据在本地是明文的,任何能打开 DevTools 的人都能看到
  • 不要在客户端存储密码、密钥等高度敏感信息
  • 如果必须存储敏感数据,应该在存入前加密,读取时解密

存储配额与异常处理

// localStorage 写入可能因配额满而抛异常
try {
  localStorage.setItem('key', largeData);
} catch (error) {
  if (error.name === 'QuotaExceededError') {
    // 清理旧数据或降级处理
    console.warn('存储空间已满');
  }
}

面试高频追问

Q: cookie 和 localStorage 最本质的区别是什么?

Cookie 会随 HTTP 请求自动发送到服务器,localStorage 不会。这决定了 cookie 适合身份认证场景,而 localStorage 适合纯客户端数据存储。

Q: 为什么不把 token 存在 localStorage 里?

localStorage 没有 HttpOnly 保护,XSS 攻击可以直接用 localStorage.getItem 读走 token。而 HttpOnly Cookie 即使页面被注入恶意脚本也无法被 JS 读取。但也有观点认为,如果已经被 XSS 了,攻击者可以直接以用户身份发请求,HttpOnly 也只是增加了攻击成本而非根本解决问题。

Q: sessionStorage 在新标签页打开同一个 URL 能共享数据吗?

直接在地址栏输入 URL 打开新标签页——不能共享。但通过 window.open 或带 target="_blank" 的链接打开——会复制一份当时的 sessionStorage 作为初始值,之后两者独立。

Q: 如何实现 localStorage 的过期机制?

localStorage 本身不支持过期时间,需要自行封装:

function setWithExpiry(key, value, ttlMs) {
  const item = { value, expiry: Date.now() + ttlMs };
  localStorage.setItem(key, JSON.stringify(item));
}

function getWithExpiry(key) {
  const raw = localStorage.getItem(key);
  if (!raw) return null;
  const item = JSON.parse(raw);
  if (Date.now() > item.expiry) {
    localStorage.removeItem(key);
    return null;
  }
  return item.value;
}

Q: IndexedDB 相比 localStorage 的核心优势在哪?

三个维度:容量(数百 MB vs 5MB)、数据类型(结构化对象 + 二进制 vs 纯字符串)、查询能力(索引 + 游标 vs 只能按 key 取值)。另外 IndexedDB 是异步的,不会阻塞主线程。

总结

  • Cookie 是唯一能自动随请求发送的存储方案,适合身份认证,但容量小、API 原始,安全属性(HttpOnly/Secure/SameSite)必须配置正确
  • localStorage 提供简单持久化键值存储,跨标签页共享,适合用户偏好等少量数据
  • sessionStorage 与 localStorage API 一致但生命周期限于标签页,适合临时状态
  • IndexedDB 是浏览器端的事务型数据库,容量大、支持结构化数据和索引,适合离线应用和大数据量场景
  • 安全上,敏感 token 优先用 HttpOnly Cookie 存储,客户端存储要做好 XSS 防护和异常处理