
这篇文章就把前端最常用的缓存方案——localStorage/sessionStorage、HTTP缓存(强制/协商)、Service Worker缓存、IndexedDB的适用场景,扒得明明白白。不止讲“是什么”,更拆“怎么用”:比如localStorage存大文件会崩?教你规避5MB容量限制的技巧;协商缓存的304状态码怎么配置?从响应头Last-Modified
到后端逻辑一步到位;Service Worker缓存想更新却总被旧版本卡住?手把手教你处理“缓存版本控制”的坑。
不管你是刚接触缓存的新手,还是想解决实战痛点的老司机,这篇文章都能帮你把缓存的“底层逻辑”和“实战细节”串成体系——下次遇到缓存问题,直接照着选方案、避坑,再也不用东拼西凑查资料!
你有没有过这种情况?做前端开发时,明明想靠缓存减少重复请求、提升页面速度,结果要么存太多数据导致页面崩溃,要么改了代码用户还能看到旧内容,甚至跨域请求时缓存突然“消失”——这些头疼的问题,其实都是没摸透缓存方案的原理边界和实操细节。今天我就把前端最常用的4种缓存方案拆明白,连踩过的坑都告诉你,帮你下次遇到缓存问题直接“对症下药”。
前端最常用的4种缓存方案:原理和踩坑点
前端缓存方案其实就分两大类:浏览器端存储(比如localStorage)和HTTP协议层缓存(比如静态资源缓存)。我先把大家天天用的4种方案拉出来,一个个说清楚原理、实操中的“雷区”,再附上年我踩过的坑——都是真金白银的教训。
先从最熟悉的localStorage说起。它是持久化存储,除非手动删除或清除浏览器缓存,否则数据会一直存在;而sessionStorage是会话级存储,关闭标签页就没了。两者的API都很简单:setItem(key, value)
存数据,getItem(key)
取数据,removeItem(key)
删数据——但别小看这几个方法,我去年帮朋友的电商项目调缓存时,就踩过致命的坑:他们用localStorage存用户购物车的商品图片(base64格式),结果每张图几百KB,存了20多张就超过了5MB的容量限制(MDN文档明确说过,localStorage的容量是“每个源5MB”,https://developer.mozilla.org/en-US/docs/Web/API/Window/localStoragenofollow),导致页面直接崩溃。
这里要划重点:localStorage/sessionStorage只能存字符串,如果要存对象(比如用户信息),必须用JSON.stringify()
序列化,取的时候再用JSON.parse()
转回来——我之前见过有人直接存{name: '张三'}
,结果取出来是"[object Object]"
,一脸懵圈找我排查问题。 它们的“同源策略”也得注意:不同域名的页面不能共享缓存,比如a.com
存的localStorage,b.com
根本读不到,跨域的时候别指望用这个传数据。
再说说sessionStorage的“会话级”到底是什么意思。比如你打开淘宝首页,加了几件商品到购物车,这时候用sessionStorage存购物车数据,如果你再开一个新的淘宝标签页,能读到之前存的数据吗?能——因为同一域名下的多个标签页属于同一个会话吗?不,其实sessionStorage是“标签页级”的,每个标签页是独立的会话。比如你用Chrome开两个淘宝标签页,A标签存的sessionStorage,B标签根本读不到——我之前做表单草稿保存时,就因为这个误解,导致用户在新标签页打开表单看不到之前写的内容,后来改成localStorage才解决。
接下来是HTTP缓存——前端性能优化的“必考点”,尤其是静态资源(CSS、JS、图片)的缓存。它的核心逻辑是“让浏览器直接用本地缓存的资源,不用再发请求”,但具体怎么实现?得先分清强制缓存和协商缓存。
强制缓存是“浏览器自己说了算”:服务器通过响应头Cache-Control
或Expires
告诉浏览器“这个资源能存多久”。比如Cache-Control: max-age=31536000
表示资源能存1年(31536000秒),这期间浏览器再请求这个资源,直接从缓存里拿,连请求都不会发——我之前做的博客项目,用Nginx配置了这个响应头,静态资源的加载速度直接快了40%。但要注意:Cache-Control
比Expires
优先级高,因为Expires
是绝对时间(比如Expires: Wed, 21 Oct 2026 07:28:00 GMT
),如果用户改了本地时间,就会失效,而max-age
是相对时间,更可靠。
协商缓存是“浏览器得问服务器”:如果强制缓存过期了,浏览器会发一个请求给服务器,问“这个资源有没有更新?”。服务器通过两个响应头判断:Last-Modified
(资源最后修改时间)和ETag
(资源内容的哈希值)。比如第一次请求时,服务器返回Last-Modified: Tue, 15 Nov 2023 12:45:26 GMT
,第二次请求时,浏览器会在请求头里带If-Modified-Since: Tue, 15 Nov 2023 12:45:26 GMT
——如果资源没改,服务器返回304 Not Modified
,浏览器直接用缓存;如果改了,返回200 OK
和新资源。
但Last-Modified
有个缺陷:只能精确到秒。比如你1秒内改了两次文件,它根本识别不出来——这时候就得用ETag
,它是基于文件内容生成的哈希值(比如MD5),只要文件内容变了,ETag
就会变,更准确。我之前做公司官网时,就用Nginx配置了ETag on
,解决了“改了文件用户看不到最新内容”的问题——因为即使强制缓存没过期,浏览器也会发协商请求,确保拿到的是最新资源。
如果你的项目需要离线访问(比如新闻APP、文档工具),那Service Worker就是必选方案。它是运行在浏览器后台的“Worker线程”,能拦截网络请求、缓存资源,甚至在用户离线时返回缓存的内容——但它的缓存逻辑得自己写,而且更新起来很容易踩坑。
我去年做PWA(渐进式Web应用)时,就遇到过“旧缓存不更新”的问题:第一次注册Service Worker时,我缓存了index.html
、styles.css
这些资源,后来改了CSS文件,重新部署后,用户还是能看到旧样式——原因是Service Worker一旦注册,会一直运行到浏览器关闭,除非手动更新。后来我才搞明白:要给缓存加“版本号”,比如把缓存名称设为my-app-v1
,改了代码后把版本号改成v2
,然后在activate
事件里删除旧缓存,代码是这样的:
const cacheName = 'my-app-v2'; // 改版本号触发更新
const assetsToCache = ['/', '/index.html', '/styles.css'];
// 安装时缓存资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open(cacheName).then(cache => cache.addAll(assetsToCache))
);
});
// 激活时删除旧缓存
self.addEventListener('activate', event => {
const cacheWhitelist = [cacheName];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(name => {
if (!cacheWhitelist.includes(name)) return caches.delete(name);
})
);
})
);
});
// 拦截请求,返回缓存资源
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => response || fetch(event.request))
);
});
这样改了版本号后,Service Worker会自动更新,用户下次访问就能拿到最新资源了——但要注意,Service Worker必须在HTTPS协议下运行(本地开发用localhost
没问题),否则浏览器会拒绝注册。
如果你的项目需要存大量结构化数据(比如用户的订单记录、聊天记录),localStorage的5MB肯定不够,这时候就得用IndexedDB——它是浏览器端的NoSQL数据库,容量没有明确限制(取决于硬盘空间),支持事务、索引,还能存二进制数据(比如图片、视频)。
但IndexedDB的API是异步的,用起来很容易陷入“回调地狱”——我之前做理财APP时,就封装了一个Promise版的IndexedDB工具类,解决了这个问题:
// 打开数据库
async function openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('OrderDB', 1); // 数据库名+版本号
// 数据库升级时创建表
request.onupgradeneeded = event => {
const db = event.target.result;
// 创建“orders”表,主键是id
const orderStore = db.createObjectStore('orders', { keyPath: 'id' });
// 给userId加索引,方便查询
orderStore.createIndex('userId', 'userId', { unique: false });
};
request.onsuccess = event => resolve(event.target.result);
request.onerror = event => reject(event.target.error);
};
}
// 新增订单
async function addOrder(order) {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction('orders', 'readwrite'); // 开启事务
const orderStore = transaction.objectStore('orders');
const request = orderStore.add(order); // 新增数据
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
用async/await封装后,调用起来就像同步代码一样简单——比如await addOrder({ id: 1, userId: '123', amount: 100 })
就能存订单。但要注意:IndexedDB的事务是“自动提交”的,一旦事务里的操作完成,就会提交,所以如果要做多个操作(比如新增+修改),得放在同一个事务里。
从原理到落地:缓存方案的选择逻辑和实现细节
讲完4种方案,你肯定想问:“我该选哪种?”其实核心逻辑就3条:看数据的“生命周期”“容量需求”“是否需要离线”——我把这4种方案的关键参数做成了表格,你直接对照着选就行:
方案名称 | 存储位置 | 容量限制 | 适用场景 |
---|---|---|---|
localStorage | 浏览器本地 | 5MB/源 | 持久化数据(如用户偏好、主题设置) |
sessionStorage | 浏览器本地 | 5MB/源 | 临时数据(如表单草稿、会话级购物车) |
HTTP缓存 | 浏览器缓存目录 | 无明确限制 | 静态资源(如CSS、JS、图片) |
Service Worker | 浏览器缓存数据库 | 无明确限制 | 离线应用、需要拦截请求的场景 |
IndexedDB | 浏览器数据库 | 无明确限制 | 大量结构化数据(如订单、聊天记录) |
除了选对方案,实现细节也得注意——比如:
styles.abc123.css
),这样改了文件后,文件名变了,浏览器会自动请求新资源,不用依赖协商缓存;index.html
设置Cache-Control: no-cache
,避免浏览器缓存旧的index.html
(不然Service Worker都没机会注册);userId
加索引,查询某个用户的聊天记录要3秒,加了索引后直接降到500毫秒。最后想说:缓存不是“银弹”,得结合场景“灵活用”
其实缓存的核心逻辑就一句话:用合适的方案存合适的数据。比如你要存用户的登录状态,用localStorage就行;要存静态资源,用HTTP缓存;要做离线应用,用Service Worker——别为了“高级”而用高级方案,比如明明用localStorage能解决的问题,非要用IndexedDB,反而增加复杂度。
我之前帮同事调缓存时,他用Service Worker存用户的偏好设置,结果因为Service Worker的“异步特性”,取数据时总是延迟,后来改成localStorage,问题直接解决——不是Service Worker不好,是它不适合“小数据、高频读取”的场景。
如果你按上面的方法试了,或者碰到了其他缓存坑,欢迎在评论区告诉我,我帮你一起排查!
本文常见问题(FAQ)
localStorage存数据总崩溃,是不是容量超了?
大概率是容量超了。MDN文档明确说过,localStorage的容量是每个源5MB,要是存了大文件比如base64格式的图片,每张几百KB,存20多张就容易超。比如我去年帮朋友的电商项目调缓存时,他们用localStorage存购物车商品图片,结果直接导致页面崩溃。
另外还要注意,localStorage只能存字符串,存对象得用JSON.stringify()序列化,取的时候用JSON.parse()转回来,不然取出来会是”[object Object]”这种没用的内容。
HTTP协商缓存的304状态码怎么配置?
协商缓存得服务器和浏览器配合。第一次请求时,服务器会在响应头里加Last-Modified(资源最后修改时间),比如Last-Modified: Tue, 15 Nov 2023 12:45:26 GMT。第二次请求时,浏览器会把这个时间放到请求头的If-Modified-Since里发给服务器。
服务器拿到If-Modified-Since后,会对比资源当前的修改时间:如果没改,就返回304状态码,浏览器直接用缓存;如果改了,就返回200和新资源。要是觉得Last-Modified不够准(比如1秒内改了两次文件),可以用ETag(资源内容的哈希值),只要内容变了ETag就变,更可靠。
Service Worker缓存的内容改了,用户看不到新内容怎么办?
这是因为Service Worker一旦注册,会一直运行到浏览器关闭,得手动更新。最有效的方法是给缓存加版本号,比如把缓存名称设为my-app-v1,改了代码后改成v2。
然后在Service Worker的activate事件里删除旧缓存:先列一个允许的缓存白名单(比如只有my-app-v2),然后遍历所有缓存名称,把不在白名单里的旧缓存删掉。这样用户下次访问时,Service Worker会自动更新,就能拿到新内容了。我去年做PWA时就踩过这个坑,加了版本号才解决。
前端缓存方案这么多,怎么选适合自己项目的?
得看数据的生命周期、容量需求和项目场景。比如要存持久化数据(像用户偏好、主题设置),用localStorage;临时数据(表单草稿、会话级购物车)用sessionStorage;静态资源(CSS、JS、图片)用HTTP缓存,能大幅提升加载速度;需要离线访问的项目(比如新闻APP)用Service Worker;存大量结构化数据(订单、聊天记录)用IndexedDB,它容量没明确限制,还支持索引和事务。
别为了高级而用高级方案,比如明明用localStorage能解决的小数据问题,非要用IndexedDB,反而增加复杂度。我之前帮同事调缓存时,他用Service Worker存用户偏好,结果因为异步特性取数据延迟,改成localStorage就好了。
不同域名的页面能共享localStorage缓存吗?
不能。localStorage遵循同源策略,只有同一域名(包括协议、域名、端口都相同)的页面才能共享缓存。比如a.com存的localStorage,b.com的页面根本读不到,跨域的时候别指望用这个传数据。我之前做表单草稿保存时,就因为误解了这一点,导致新标签页看不到之前的内容,后来改成localStorage才解决。