一份由前端开发者写给前端开发者的缓存指南
本文不是官方文档的翻译,也不是学术论文,而是一个在前端坑里摸爬滚打多年的人,对“浏览器缓存”这件事的总结与思考。希望它能帮你少走弯路。

为什么我们需要缓存?
想象一下这个场景:你打开一个新闻网站,第一次加载花了 5 秒。你刷新页面,如果一切从头再来,又是 5 秒。第三次、第四次……你会立刻关掉这个网站。
缓存的存在,本质上是为了两个目的:
提升用户体验 – 更快的加载速度,更流畅的交互
减轻服务器压力 – 减少重复请求,节省带宽和计算资源
从技术角度说,每次请求都去服务器拉取完整的资源,在当今 Web 应用动辄几 MB 的时代,是不可接受的。缓存就是那个“聪明的中间人”,帮你记住已经拿过的东西。
缓存里到底存了些什么?
很多人以为缓存就是“存文件”,其实更细分:
HTML 文档 – 页面结构
CSS 样式表 – 控制视觉表现
JavaScript 文件 – 逻辑与交互
图片、字体、视频等静态资源 – 通常体积最大
API 响应数据(通过 Service Worker 或内存缓存)
甚至整个页面(通过离线缓存或 PWA)
关键点:缓存不是“一个”东西,而是分布在浏览器不同层级的多套机制。
缓存的类型与层级
1. Memory Cache(内存缓存)
位置:RAM
生命周期:会话期间,标签页关闭即消失
特点:极快,用于当前页面已经加载过的资源(如样式表、脚本)
你无法直接控制它,浏览器自动管理
2. Disk Cache(磁盘缓存)
位置:硬盘
生命周期:可长期存在(根据 HTTP 头决定)
特点:容量大,速度比内存慢但比网络快
这是我们可以通过 HTTP 头精细控制的主要缓存
3. Service Worker Cache
位置:独立于浏览器常规缓存
生命周期:由代码控制,可持久化
特点:可以拦截请求、自定义缓存策略,是实现离线应用的核心
示例:PWA 应用即使没网络也能加载
4. Push Cache(HTTP/2 Server Push)
位置:浏览器特殊缓存区
特点:服务器推送的资源,在页面实际需要前就预先缓存
使用场景较少,但在性能优化中有特殊价值
HTTP 缓存头:我们的主要工具
这是前端最能掌控的部分。几个关键头字段:
Cache-Control
最重要的缓存控制头,指令众多:
Cache-Control: max-age=3600, public, must-revalidate
常用指令:
max-age=<seconds>:资源有效期(秒)public/private:是否允许代理服务器缓存no-cache:可缓存,但每次都要向服务器验证no-store:完全不缓存(敏感数据用这个)must-revalidate:过期后必须向服务器验证immutable:资源永不变,刷新也直接用缓存(适合版本化文件)
ETag / Last-Modified(验证机制)
当缓存过期或需要验证时使用:
ETag:资源的哈希值(如
"33a64df551425fcc55e4d42a148795d9f25f89d4")Last-Modified:最后修改时间
浏览器会发送 If-None-Match(ETag 值)或 If-Modified-Since(时间),服务器返回 304(未修改)则使用缓存,返回 200 + 新内容则更新缓存。
Expires(已逐渐被取代)
老式绝对时间过期头:
Expires: Wed, 21 Oct 2026 07:28:00 GMT
问题:依赖客户端时钟准确,现多用 max-age。
前端如何设置缓存?
1. 服务器配置(推荐)
在 Nginx/Apache/CDN 配置中设置:
# Nginx 示例
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location /index.html {
add_header Cache-Control "no-cache";
}
2. 后端代码设置
Node.js(Express)示例:
app.use(express.static('public', {
maxAge: '1y',
setHeaders: (res, path) => {
if (path.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache');
} else if (path.match(/\.(js|css|png|jpg)$/)) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
}
}
}));
3. 前端构建工具的缓存破坏(Cache Busting)
这是解决“代码更新后用户还用旧缓存”的关键技术:
Webpack 示例(文件名带哈希):
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js'
}
生成的文件如:main.a3f8c7d2.js,内容一变,哈希就变,URL 不同自然绕过缓存。
4. Service Worker 自定义缓存
更细粒度的控制:
// service-worker.js
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// 缓存优先,网络兜底策略
return response || fetch(event.request);
})
);
});
缓存容易出问题的地方(坑!)
1. “我更新了代码,用户怎么还是旧的?”
原因:静态资源设置了长期缓存,但文件名没变。
解决方案:
使用内容哈希文件名(如上文 Webpack 配置)
HTML 文件设置
no-cache或很短max-age,让 HTML 去拉取新版本资源使用查询参数(不推荐):
script.js?v=2,但有些代理服务器会忽略查询参数
2. 缓存污染
场景:用户 A 登录后,页面缓存了个人数据,用户 B 打开看到了 A 的信息。
解决方案:
私人数据设置
Cache-Control: private, no-storeAPI 响应谨慎缓存,特别是涉及用户状态的
3. 缓存过度导致内存/磁盘占用高
现象:浏览器变卡,特别是单页应用长时间不刷新。
解决方案:
合理设置
max-age,不是所有资源都需要 1 年缓存使用
Clear-Site-Data头(需要时让浏览器清除)用户教育:偶尔清理浏览器缓存
4. 304 请求的“假节约”
误解:304 响应没 body,所以很快。
现实:304 仍然需要一次完整的 HTTP 请求-响应往返,对于距离服务器远的用户,延迟依然明显。
优化:对确定不变的文件使用 immutable,避免验证请求。
5. CDN 缓存与源站不一致
场景:你在源站更新了文件,但 CDN 节点还缓存着旧版本。
解决方案:
CDN 刷新(手动或 API)
使用不同 URL(哈希文件名自动解决)
设置合适的 CDN 缓存规则
缓存策略选择指南
根据资源类型选择合适的策略:
调试缓存:开发者工具实战
Chrome DevTools 的 Network 面板是你的战场:
Size 列显示:
(memory cache)/(disk cache)– 从缓存读取具体数字 – 从网络下载
检查请求头与响应头:
确认
Cache-Control、ETag是否正确观察
If-None-Match等验证头
Disable cache 选项:
开发时勾选,避免缓存干扰调试Application → Clear storage:
可以清除特定站点的缓存,用于测试
高级话题:缓存与性能指标的关联
缓存直接影响核心 Web 性能指标:
首次内容绘制(FCP):缓存 HTML/CSS 可显著提升
最大内容绘制(LCP):缓存大图或字体可大幅改善
累积布局偏移(CLS):缓存字体可避免布局抖动
首次输入延迟(FID):缓存关键 JS 可减少主线程阻塞
建议:在性能监控中观察缓存命中率,将其作为关键优化指标。
最佳实践总结
分层策略:不同资源不同缓存策略,别一刀切
哈希文件名:静态资源带内容哈希,安全使用长期缓存
HTML 不长期缓存:让它作为版本协调器
API 谨慎缓存:特别是涉及状态变化的
监控与测试:用真实用户数据验证缓存效果
降级方案:缓存失效时,应用仍能正常工作(即使慢一点)
文档化:团队的缓存策略要文档化,避免不同成员配置冲突
延伸阅读
Caching Best Practices – Jake Archibald 的经典文章
最后的话
缓存不是银弹,而是一门平衡艺术——在新鲜度与性能之间、在服务器压力与用户体验之间、在开发便利与部署复杂度之间找到最佳点。
好的缓存策略应该是:
用户无感知的:他们只感觉到“快”,不知道背后有缓存
开发者可控的:出问题时我们知道如何排查和修复
业务匹配的:电商网站的缓存策略和新闻网站肯定不同
希望这篇文档能帮你建立对浏览器缓存的系统理解。在实际项目中,最好的学习方式是:设置一个策略 → 测试效果 → 分析数据 → 迭代优化。
Happy caching! 🚀