你有没有想过这个问题:为什么在网页上勾选了"记住我",下次打开还是登录状态?你改了个主题设置,关掉浏览器再打开,主题还在?浏览器是怎么记住这些数据的?
今天,用**"收纳房间"**的故事,来讲讲浏览器存储。
原文地址
墨渊书肆/5MB vs 4KB vs 无限大:浏览器存储谁更强?
浏览器是怎么"装东西"的?
想象一下你家要装修,需要各种收纳工具:
-
贴身口袋:装点小东西,随时能用
-
床头柜:装常用物品,随取随用
-
衣柜:装换季衣服,大容量
-
仓库:存大件物品,最大但找起来麻烦
浏览器存储也是这个道理。不同的数据,要用不同的"收纳工具"。
Cookie — 贴身口袋
像个口袋,随身带
Cookie 是最"古老"的浏览器存储方案。它最大的特点是——会自动跟着请求一起发出去。
就像你出门带了个口袋,里面装着身份证、银行卡。进任何一家店,都要掏出身份证证明身份。
浏览器也是:每次请求网页,Cookie 都自动带上,服务器就知道"哦,这是张三的浏览器"。
Cookie 的特点
| 属性 |
值 |
像什么 |
| 容量 |
~4KB |
口袋里只能装这么多 |
| 发送 |
自动随请求发送 |
出门就带 |
| 生命周期 |
可设置过期时间 |
可以设有效期 |
| 访问 |
JS和服务器都能读 |
谁都能用 |
Cookie 的使用场景
-
登录状态:"记住我"功能
-
购物车:逛淘宝加购物车
-
追踪分析:埋点上报
Cookie 的代码
// 设置Cookie
document.cookie = "username=张三; expires=Fri, 31 Dec 2026 23:59:59 GMT; path=/";
// 读取Cookie
console.log(document.cookie); // "username=张三; theme=dark"
Cookie 的安全问题
Cookie 虽然方便,但有几个安全属性要注意:
| 属性 |
作用 |
什么意思 |
| HttpOnly |
JS无法访问 |
口袋上锁了,店员碰不到 |
| Secure |
只在HTTPS发送 |
只能用加密通道 |
| SameSite |
防止CSRF攻击 |
别人拿不到你的卡 |
深入了解 Cookie 🔬
Cookie 是怎么工作的?
Cookie 由 HTTP 协议定义,通过 Set-Cookie 响应头设置:
HTTP/1.1 200 OK
Set-Cookie: sessionId=abc123; Path=/; HttpOnly; Secure; SameSite=Strict
HTTP/1.1 200 OK
Cookie: sessionId=abc123
浏览器怎么存 Cookie?
每个浏览器都有自己的存储方式:
| 浏览器 |
存储位置 |
| Chrome/Edge |
SQLite 数据库 (%APPDATA%\Local\Google\Chrome\User Data\Default\Cookies) |
| Firefox |
JSON 文件 (cookies.sqlite) |
| Safari |
二进制文件 |
Cookie 的发送规则?
浏览器根据 Domain + Path + SameSite 三个规则决定是否发送:
// 例如:Cookie 设置为 Domain=example.com, Path=/admin
// 会发送给:
// ✅ example.com/admin
// ✅ example.com/admin/users
// ❌ example.com/ (path不匹配)
// ❌ other.com/admin (domain不匹配)
Session Cookie vs 持久 Cookie?
# 会话Cookie(没有Expires/Max-Age)
Set-Cookie: sessionId=abc123
# 关掉浏览器就失效
# 持久Cookie
Set-Cookie: sessionId=abc123; Expires=Wed, 01 Jan 2027 00:00:00 GMT
# 有效期内都有效
LocalStorage — 床头柜
容量大,但不主动发
LocalStorage 是 HTML5 引入的存储方案。最大的特点:不会随请求发出去。
就像床头柜——你把东西放里面,下次进门直接拿,不用每次出门都背着。
LocalStorage 的特点
| 属性 |
值 |
像什么 |
| 容量 |
~5MB/域 |
床头柜大小 |
| 发送 |
不随请求发送 |
不随身带 |
| 生命周期 |
永久存储 |
除非搬家(手动删除) |
| API |
同步操作 |
马上拿到 |
LocalStorage 的使用场景
-
主题设置:深色/浅色模式
-
用户偏好:字体大小、语言设置
-
数据缓存:接口数据本地缓存
LocalStorage 的代码
// 设置
localStorage.setItem('username', '张三');
localStorage.setItem('theme', 'dark');
// 读取
const theme = localStorage.getItem('theme'); // 'dark'
// 删除
localStorage.removeItem('theme');
// 清空
localStorage.clear();
// 遍历
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
console.log(`${key}: ${localStorage.getItem(key)}`);
}
LocalStorage 的缺点
-
同步操作:大量数据会卡界面
-
只能存字符串:对象要转成 JSON
-
容量有限:5MB 对大数据不够
深入了解 LocalStorage 🔬
同源策略限制
LocalStorage 遵循同源策略:
✅ http://example.com 和 https://example.com 共享同一个Storage
✅ http://example.com:8080 和 http://example.com:3000 不共享(端口不同)
✅ http://www.example.com 和 http://example.com 不共享(子域名不同)
存储配额
实际容量取决于浏览器和磁盘空间,Chrome 默认是 5MB(可申请更多):
// 查询当前配额和使用量
navigator.storage.estimate().then(({ usage, quota }) => {
console.log(`已使用: ${(usage / 1024 / 1024).toFixed(2)} MB`);
console.log(`总配额: ${(quota / 1024 / 1024).toFixed(2)} MB`);
});
// 请求更大的存储空间(需要用户授权)
navigator.storage.persist().then((granted) => {
console.log('永久存储权限:', granted);
});
为什么 LocalStorage 是同步的?
因为 LocalStorage 读取是直接读磁盘。如果数据量大,同步读取会阻塞主线程:
// ❌ 错误:大数据量时卡界面
localStorage.setItem('bigData', JSON.stringify(largeArray));
// ✅ 更好:拆分存储或用 IndexedDB
SessionStorage — 抽屉
只在当前标签页有效
SessionStorage 和 LocalStorage 几乎一样,唯一的区别是——关闭标签页就没了。
就像抽屉里的东西,只有在这个房间能用。换到另一个房间(另一个标签页),抽屉里的东西就不在了。
SessionStorage 的特点
属性 |
值 |
和LocalStorage的区别 |
| 容量 |
~5MB/域 |
一样 |
| 作用域 |
仅当前标签页 |
❌ 跨标签页不共享 |
| 生命周期 |
关闭标签页失效 |
❌ 不能持久保存 |
SessionStorage 的使用场景
-
表单草稿:填写到一半的表单
-
临时状态:当前页面的操作状态
SessionStorage 的代码
// 用法和LocalStorage完全一样
sessionStorage.setItem('draft', JSON.stringify({ title: '我的文章', content: '...' }));
关键区别
// 标签页A中设置
sessionStorage.setItem('key', 'value');
localStorage.setItem('key', 'value');
// 在标签页B中读取
sessionStorage.getItem('key'); // null ❌
localStorage.getItem('key'); // 'value' ✅
深入了解 SessionStorage 🔬
iframe 共享问题
注意:同一个标签页中的 iframe 会共享 SessionStorage(因为是同一个浏览器标签页):
// 父页面
sessionStorage.setItem('shared', 'value');
// iframe 内可以读取到
console.log(sessionStorage.getItem('shared')); // 'value'
sessionStorage 在隐私模式下
-
Chrome 无痕模式:
sessionStorage 仍然存在,但标签页关闭后失效
-
Firefox 隐私窗口:完全隔离,每个新窗口都是新的
sessionStorage
和 LocalStorage 的性能对比
两者都是同步 API,性能特性相同。但 SessionStorage 因为数据不持久,有时候比 LocalStorage 更适合存临时数据。
IndexedDB — 仓库
浏览器里的数据库
IndexedDB 是浏览器内置的数据库。容量巨大,能存文件、音频、视频这些大东西。
就像仓库——你家装修工具、电风扇、行李箱都放这儿。东西多,但找起来要翻半天。
IndexedDB 的特点
| 属性 |
值 |
像什么 |
| 容量 |
很大(取决于磁盘) |
仓库,接近无限 |
| 数据类型 |
什么都能存 |
不挑东西 |
| API |
异步操作 |
异步,不卡界面 |
| 查询 |
支持索引 |
能分类查找 |
IndexedDB 的使用场景
-
离线数据:PWA离线应用
-
多媒体存储:图片、音频、视频缓存
-
复杂数据:需要索引查询的数据
IndexedDB 的代码
// 打开数据库
const request = indexedDB.open('myDatabase', 1);
// 创建表(对象存储)
request.onupgradeneeded = (event) => {
const db = event.target.result;
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('name', 'name', { unique: false });
store.createIndex('email', 'email', { unique: true });
};
// 添加数据
request.onsuccess = (event) => {
const db = event.target.result;
const tx = db.transaction(['users'], 'readwrite');
const store = tx.objectStore('users');
store.add({ id: 1, name: '张三', email: 'zhangsan@example.com' });
store.add({ id: 2, name: '李四', email: 'lisi@example.com' });
};
// 查询数据
const getRequest = store.get(1);
getRequest.onsuccess = () => {
console.log('查询结果:', getRequest.result);
};
// 使用索引查询
const index = store.index('name');
const indexRequest = index.get('张三');
indexRequest.onsuccess = () => {
console.log('索引查询结果:', indexRequest.result);
};
IndexedDB 的缺点
-
API 复杂:需要写一堆回调
-
学习成本高:概念多(数据库、表、事务、索引)
深入了解 IndexedDB 🔬
数据库版本和升级
const request = indexedDB.open('myDatabase', 2); // 版本号从1升到2
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建新存储
if (!db.objectStoreNames.contains('products')) {
db.createObjectStore('products', { keyPath: 'id' });
}
// 删除旧存储
if (db.objectStoreNames.contains('oldData')) {
db.deleteObjectStore('oldData');
}
};
事务的原子性
const tx = db.transaction(['users', 'orders'], 'readwrite');
// 两个操作在一个事务里,要么全成功,要么全失败
tx.objectStore('users').add({ id: 1, name: '张三' });
tx.objectStore('orders').add({ id: 1, userId: 1, product: '电脑' });
tx.oncomplete = () => console.log('事务成功');
tx.onerror = () => console.log('事务失败,全部回滚');
游标遍历大量数据
const tx = db.transaction(['users'], 'readonly');
const store = tx.objectStore('users');
const cursor = store.openCursor();
cursor.onsuccess = (event) => {
const cur = event.target.result;
if (cur) {
console.log('用户:', cur.value.name);
cur.continue(); // 继续下一个
}
};
Promise 封装(更简洁的写法)
function openDB(name, version) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name, version);
request.onupgradeneeded = (e) => resolve(e.target.result);
request.onsuccess = (e) => resolve(e.target.result);
request.onerror = (e) => reject(e.target.error);
});
}
// 使用
const db = await openDB('myDatabase', 1);
const tx = db.transaction('users', 'readwrite');
await tx.objectStore('users').add({ id: 1, name: '张三' });
Cache API — 集装箱
Service Worker 的专属工具
Cache API 是 Service Worker 的一部分,专门用来缓存网络请求。
就像集装箱——你坐飞机带不了大件行李,但可以用集装箱海运。东西多、个头大,但只能走特定渠道。
Cache API 的特点
| 属性 |
值 |
像什么 |
| 容量 |
很大 |
集装箱,装得多 |
| 存储内容 |
Request/Response 对 |
整套打包 |
| 生命周期 |
手动管理 |
不用就扔 |
| API |
异步操作 |
不卡界面 |
Cache API 的使用场景
-
离线应用:把整个网站缓存下来
-
性能优化:缓存静态资源
-
Service Worker:配合SW实现缓存策略
Cache API 的代码
// 在Service Worker中使用
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((cachedResponse) => {
return cachedResponse || fetch(event.request);
})
);
});
// 打开缓存
caches.open('my-cache').then((cache) => {
cache.addAll([
'/css/style.css',
'/js/app.js',
'/images/logo.png'
]);
});
// 缓存特定请求
cache.put(request, response);
// 删除缓存
caches.delete('my-cache');
深入了解 Cache API 🔬
缓存策略
// Cache First(缓存优先)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => response || fetch(event.request))
);
});
// Network First(网络优先)
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.catch(() => caches.match(event.request))
);
});
// Stale-While-Revalidate(先返回缓存,同时更新缓存)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open('my-cache').then((cache) => {
return cache.match(event.request).then((response) => {
const fetchPromise = fetch(event.request).then((networkResponse) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
return response || fetchPromise;
});
})
);
});
Cache API 和 cookies
Cache API 存储的是完整的 Request/Response 对,不只是 body:
// 缓存时包含了headers、status等所有信息
cache.match(request).then((response) => {
console.log(response.status); // 200
console.log(response.headers.get('content-type')); // 'text/html'
});
缓存清理策略
// 删除指定缓存
caches.delete('old-cache');
// 清理所有版本,只保留最新的
caches.keys().then((cacheNames) => {
Promise.all(
cacheNames
.filter((name) => name.startsWith('app-') && name !== 'app-v2')
.map((name) => caches.delete(name))
);
});
Storage Event — 跨标签页喊话
标签页之间能"喊话"
当 LocalStorage 发生变化时,其他同源的标签页会收到通知。
就像你在客厅喊了一句"饭好了",厨房的人、卧室的人都能听到。
Storage Event 的代码
// 标签页A中监听
window.addEventListener('storage', (event) => {
console.log('key:', event.key); // 变化的键
console.log('oldValue:', event.oldValue); // 旧值
console.log('newValue:', event.newValue); // 新值
console.log('url:', event.url); // 触发变化的页面URL
console.log('storageArea:', event.storageArea); // localStorage 或 sessionStorage
});
// 标签页B中修改
localStorage.setItem('theme', 'dark'); // 标签页A会收到通知
使用场景
-
多标签页同步:一个标签页登录,其他标签页同步登录状态
-
状态广播:跨标签页的状态通知
深入了解 Storage Event 🔬
Storage Event 的触发条件
// ✅ 会触发 storage 事件
localStorage.setItem('key', 'value');
localStorage.removeItem('key');
localStorage.clear();
// ❌ 不会触发 storage 事件(同一个标签页)
// Storage Event 只在「其他标签页」变化时触发
SessionStorage 也会触发?
注意:SessionStorage 本身不跨标签页共享,但 Storage Event 只监听 localStorage 的变化。
// SessionStorage 变化不会触发 storage 事件
sessionStorage.setItem('key', 'value'); // 不会触发其他标签页
// localStorage 变化会触发
localStorage.setItem('key', 'value'); // 其他标签页会收到通知
隐私模式下不触发
在无痕/隐私模式下,Storage Event 不会触发,这是浏览器的隐私保护机制。
横向对比
| 特性 |
Cookie |
LocalStorage |
SessionStorage |
IndexedDB |
Cache API |
| 容量 |
~4KB |
~5MB |
~5MB |
很大 |
很大 |
| 生命周期 |
可设置 |
永久 |
关闭失效 |
永久 |
手动 |
| 发送 |
自动发 |
不发 |
不发 |
不发 |
不发 |
| API |
简单 |
同步简单 |
同步简单 |
异步复杂 |
异步 |
| 数据类型 |
字符串 |
字符串 |
字符串 |
所有可序列化 |
Request/Response |
| 跨标签页 |
共享 |
共享 |
不共享 |
共享 |
不共享 |
怎么选?
| 场景 |
推荐 |
| 需要服务器读取 |
Cookie |
| 存用户偏好、主题 |
LocalStorage |
| 临时状态、标签页隔离 |
SessionStorage |
| 大数据、离线存储 |
IndexedDB |
| Service Worker缓存 |
Cache API |
注意事项
1. 不要存敏感信息
LocalStorage 可以被 JS 访问,XSS 攻击能偷走数据。敏感信息用 HttpOnly Cookie。
2. 存储配额
浏览器对存储有限制,可以用 API 查询:
navigator.storage.estimate().then(({ usage, quota }) => {
console.log('已用:', (usage / 1024 / 1024).toFixed(2), 'MB');
console.log('总配额:', (quota / 1024 / 1024).toFixed(2), 'MB');
});
3. 序列化问题
LocalStorage 和 SessionStorage 只能存字符串,对象要转 JSON:
// 存
localStorage.setItem('data', JSON.stringify({ name: '张三' }));
// 取
const data = JSON.parse(localStorage.getItem('data'));
4. 同步 API 的性能问题
LocalStorage/SessionStorage 是同步操作,大量数据会阻塞主线程:
// ❌ 不好:大量数据卡界面
for (let i = 0; i < 10000; i++) {
localStorage.setItem(`key${i}`, `value${i}`);
}
// ✅ 更好:用 IndexedDB 存储大量数据
总结
| 存储方式 |
像什么 |
特点 |
| Cookie |
口袋 |
小、随请求发、安全属性多 |
| LocalStorage |
床头柜 |
5MB、不发送、永久 |
| SessionStorage |
抽屉 |
5MB、不发送、仅标签页 |
| IndexedDB |
仓库 |
巨大、异步、复杂 |
| Cache API |
集装箱 |
Service Worker专用 |
选对"收纳工具",数据管理更轻松。
写在最后
现在你应该明白了:
-
Cookie = 口袋,随身带、自动发送、容量小
-
LocalStorage = 床头柜,大容量、不发送、永久保存
-
SessionStorage = 抽屉,只在当前标签页有效
-
IndexedDB = 仓库,最大但操作复杂
-
Cache API = 集装箱,Service Worker专用
下次你在网页上勾选"记住我",或者调整了主题设置——你就知道浏览器是用哪种"收纳工具"帮你存的了。