前端缓存
Category(分类): Browser, Network, Performance Status: 已更新
缓存是前端性能和部署稳定性中很重要的一环。原文把 HTTP 缓存、CDN、Service Worker、本地存储、DNS 缓存和内存/磁盘缓存放在一起讲,并保留了不少历史实践。本文将这些内容重新整理:过时的结论保留为历史背景,错误的说法直接修正。
一、先区分几种“缓存”
日常说的“前端缓存”至少可能指下面几类东西:
| 类型 | 保存的内容 | 是否属于 HTTP 缓存 | 典型控制方式 |
|---|---|---|---|
| 浏览器 HTTP 缓存 | HTTP 响应(HTML、JS、CSS、图片等) | 是 | Cache-Control、ETag、Last-Modified |
| CDN/反向代理缓存 | 可共享的 HTTP 响应 | 是,共享缓存 | 响应头、CDN 规则、失效 API |
| Service Worker Cache | Request/Response 对 | 是一种可编程托管缓存 | Cache API、Service Worker 策略 |
| Memory/Disk Cache | 浏览器实现 HTTP 缓存的内部存储 | 是实现细节 | 浏览器自行决定 |
| Cookie | 少量键值,自动随请求发送 | 否 | Set-Cookie 属性 |
| Web Storage | 字符串键值 | 否 | localStorage、sessionStorage |
| IndexedDB | 异步结构化数据 | 否 | IndexedDB 事务 |
| BFCache | 页面历史快照 | 否,不是资源缓存 | 浏览器自动管理 |

localStorage、sessionStorage 和 Cookie 是客户端存储,不应该笼统地称为 HTTP 缓存。它们适合保存少量状态或业务数据,但不是把所有网络资源“缓存起来”的替代方案;敏感令牌也不能因为放进了本地存储就变得安全。
容量不是固定常数
原文中的“localStorage 约 5 MB、Cookie 约 4 KB、IndexedDB 至少 250 MB”只能作为历史经验,不能当成规范保证:
- Cookie 的限制通常按单个 Cookie 和请求头大小计算,浏览器和服务端还可能有总量限制;
- Web Storage 通常是每源几 MB 级别,并且同步读写;
- IndexedDB、Cache Storage 等配额由浏览器根据磁盘空间、站点使用情况、隐私模式和用户策略动态决定;
- 存储满、用户清理、浏览器回收和隐私模式都可能导致写入失败。
二、缓存能解决什么问题
优点
- 减少网络传输和带宽成本;
- 降低 CDN、源站和数据库的压力;
- 更快地加载重复访问的资源;
- 在离线或弱网场景下,通过 Service Worker 提供有限的离线能力;
- 通过内容指纹和长缓存让静态资源获得较高的缓存命中率。
缺点和风险
- 过期策略错误会让用户看到旧页面或旧接口;
- 旧 HTML 和新 JS/CSS 不匹配可能导致白屏;
- 共享缓存错误地保存个性化响应会造成数据泄露;
- 缓存占用磁盘、内存或配额;
- Service Worker 的错误策略可能让错误响应、旧资源和离线页面长期存在;
- 缓存使服务器暂时失去对某个 URL 的控制,长时间
max-age的资源不能被源站直接“召回”。
缓存不是越多越好,而是要明确资源的更新频率、是否个性化、是否允许共享以及失败时的降级策略。
三、HTTP 缓存的基本流程

一次请求大致经历:
- 浏览器或中间缓存根据 URL、请求方法、请求头等信息查找已存储响应;
- 如果响应仍然 fresh(新鲜),可以直接使用,不需要访问源站;
- 如果响应已 stale(陈旧),但有
ETag或Last-Modified,发送条件请求进行验证; - 资源未变化时,服务器返回
304 Not Modified,浏览器继续使用已有响应体; - 资源变化时返回
200和新响应,缓存按照新的响应头更新; - 没有可用缓存或策略要求重新获取时,正常发送网络请求。
“强缓存”和“协商缓存”是前端文章常用的教学分类:
- 强缓存:在新鲜期内直接复用,不向服务器验证;
- 协商缓存:缓存已陈旧或被
no-cache要求验证,发送条件请求,由服务器决定复用还是返回新内容。
HTTP 规范更常用 fresh/stale、validation/revalidation 等术语。强缓存命中时 DevTools 可能显示 200 (from memory cache) 或 200 (from disk cache),但这些显示属于浏览器实现,不是 HTTP 状态码语义。
四、Expires 和 Cache-Control
1. Expires:历史兼容字段

Expires 使用绝对时间:
Expires: Wed, 22 Nov 2028 08:41:00 GMT
它受客户端时钟偏差影响,HTTP/1.1 后更推荐使用 Cache-Control: max-age。如果两者同时存在,Cache-Control 中的 max-age 通常优先于 Expires。
Expires 并没有从协议中消失;为了兼容旧客户端,有时仍会和 Cache-Control 一起发送,但新项目不应只依赖它。
2. Cache-Control:现代缓存主入口

常见响应头:
Cache-Control: public, max-age=31536000, immutable
max-age 表示响应从生成时间起允许保持新鲜的秒数。它不是“服务器收到请求后再倒计时”,共享缓存还会依据 Age 等信息计算当前响应年龄。
常见响应指令
| 指令 | 含义 |
|---|---|
max-age=N | 响应在 N 秒内可视为新鲜 |
s-maxage=N | 共享缓存(如 CDN)使用的 max-age,通常优先于 max-age |
public | 即使存在某些通常限制共享缓存的条件,也明确允许共享缓存;仍要确认内容可共享 |
private | 只能保存到私有缓存,适合个性化响应 |
no-cache | 可以存储,但复用前必须向服务器验证 |
no-store | 不应存储该响应;不能理解为清除已经存在的旧缓存 |
must-revalidate | 陈旧后必须验证,不能在源站不可用时随意复用 |
immutable | 内容在新鲜期内不会改变,减少不必要的重新验证 |
stale-while-revalidate | 允许在后台验证时短暂返回陈旧响应,需看浏览器/CDN 支持 |
stale-if-error | 出错时允许使用陈旧响应,需评估内容风险 |
常见请求指令
请求中的 Cache-Control 也有自己的语义,不能和响应指令混为一谈:
no-cache:要求本次请求不要直接复用未经验证的响应;no-store:要求本次请求和响应不要被缓存;max-age=0:要求不要使用年龄大于 0 的响应,常用于刷新语义;max-stale=N:允许使用不新鲜不超过 N 秒的响应;min-fresh=N:只接受至少还能新鲜 N 秒的响应;only-if-cached:只使用已有缓存,不能满足时可能返回504;no-transform:中间缓存不应改变媒体类型或内容编码等。
3. no-cache 和 no-store 不是一回事
这是原文反复强调、但很容易被记错的部分:
# 可以保存,但每次复用前验证
Cache-Control: no-cache
# 不要保存这次响应
Cache-Control: no-store
no-cache 并不等于“没有缓存”,它常常会产生带 If-None-Match 或 If-Modified-Since 的请求,并可能收到 304。no-store 才是禁止保存这次响应的主要指令,但它也不会自动删除浏览器里已经存在的旧响应。
对于登录后的个性化页面,常见组合是:
Cache-Control: private, no-cache
对于密码重置、支付结果等高敏感响应,可以考虑:
Cache-Control: no-store
不过不要把所有响应都设置成 no-store。过度使用会损失 HTTP 缓存和 BFCache 等平台能力;应依据实际隐私和新鲜度要求选择。
五、协商缓存:Last-Modified 和 ETag
1. Last-Modified / If-Modified-Since
第一次返回资源时:
HTTP/1.1 200 OK
Last-Modified: Tue, 22 Feb 2028 22:00:00 GMT
Cache-Control: no-cache
缓存陈旧后,客户端可能发送:
GET /app.js HTTP/1.1
If-Modified-Since: Tue, 22 Feb 2028 22:00:00 GMT
如果服务器判断资源没有变化,返回:
HTTP/1.1 304 Not Modified
这种方式实现简单,静态文件服务器通常可以直接从文件修改时间生成它。但它存在局限:HTTP 日期精度通常是秒,分布式机器的文件时间也可能不一致,文件内容未变但修改时间改变时会造成无效验证。
2. ETag / If-None-Match
ETag 是服务器生成的实体标签,不要求一定是文件内容哈希,也可以是版本号、大小和修改时间的组合:
HTTP/1.1 200 OK
ETag: "app-2028-03-01-abc123"
Cache-Control: no-cache
再次验证时:
GET /app.js HTTP/1.1
If-None-Match: "app-2028-03-01-abc123"
一致返回 304,不一致返回新资源。ETag 不是加密,也不能独立提供访问控制。
ETag 有强验证和弱验证:
ETag: "strong-value"
ETag: W/"weak-value"
强验证器要求表示的内容在协议要求的意义下完全一致;弱验证器只表示语义上可视为相同,不能用于所有需要字节级一致的场景。弱 ETag 不等于“取文件一部分做哈希”,具体生成方式由服务端决定。
3. 两个验证器同时存在时
现代服务端通常可以同时发送 ETag 和 Last-Modified:
ETag: "v42"
Last-Modified: Tue, 22 Feb 2028 22:00:00 GMT
若请求同时携带 If-None-Match 和 If-Modified-Since,If-None-Match 对条件验证具有优先地位。实际框架可能封装了具体判断逻辑,但不能简单写成“ETag 永远绝对优先”或“Last-Modified 已经没用”。Last-Modified 还可用于爬虫、内容管理和诊断。
六、缓存版本更新:内容指纹
对构建产物使用内容指纹是现代前端最常见的更新策略:
app.8d31c2.js
styles.4fa91e.css
logo.23c0aa.svg
资源内容变化,文件名也变化,旧 URL 的长缓存不会影响新版本:
Cache-Control: public, max-age=31536000, immutable
原文介绍了 Webpack 的 hash、chunkhash 和 contenthash:
hash通常与整个构建相关,一个文件变化可能让所有文件名变化;chunkhash根据 chunk 依赖生成,仍可能受共享 chunk 影响;contenthash根据具体内容生成,更适合让独立 CSS/JS 长期复用。
这些概念仍有学习价值,但 Vite、Rollup、Rspack 和其他构建工具的命名与分包实现可能不同,实际应检查构建产物。
HTML 为什么通常不能一年强缓存
HTML 入口通常引用最新的 JS/CSS 文件名,因此一般设置:
Cache-Control: no-cache
它允许缓存 HTML,但每次使用前验证,版本更新时能拿到新的资源引用。也可以使用较短的 max-age,配合 CDN 失效和原子部署。
如果 HTML 也使用一年 max-age,用户可能长时间拿到旧入口;即使新静态资源已经部署,旧 HTML 仍会请求旧文件。部署时还要避免先发布 HTML 后发布它引用的资源,可以采用资源先发布、HTML 最后切换的原子发布顺序。
七、Vary、认证和缓存键
缓存不能只看 URL。若响应会因为请求头变化,服务端需要使用 Vary 告诉缓存如何区分:
Vary: Accept-Encoding
Vary: Accept-Language
常见用途:
Accept-Encoding:区分 Brotli、Gzip 和未压缩响应;Accept-Language:区分语言内容;Accept:区分媒体类型。
不建议轻易使用 Vary: User-Agent,因为 User-Agent 组合很多,会显著降低命中率。若响应个性化,优先使用 Cache-Control: private,不要只因为请求带 Cookie 就假设所有缓存实现都会自动安全隔离。
还要区分浏览器私有缓存、CDN 共享缓存和 CDN 自定义缓存键。不要让含有用户资料、权限、购物车或支付信息的响应进入可被其他用户复用的共享缓存。
八、CDN 缓存
CDN 通常作为共享或托管缓存,用户请求先到边缘节点:
- CDN 按域名、路径、查询参数、请求头和自定义规则查找缓存;
- 命中且新鲜时直接返回,并可能带
Age、X-Cache等诊断头; - 未命中或陈旧时回源;
- CDN 根据响应头和自身规则保存响应,再返回给用户。
CDN 不只是“找最近服务器”,还涉及路由、TLS、压缩、缓存键、回源、失效、鉴权、Range 请求和源站保护。不同 CDN 的默认策略不同,不能只看浏览器的 Cache-Control。
可考虑为共享缓存设置:
Cache-Control: public, max-age=60, s-maxage=600
如果 CDN 支持,也可使用供应商的 CDN-Cache-Control 或 Surrogate-Control。发生紧急版本回滚时,使用 CDN 的 purge API;不要指望已经命中长 max-age 的客户端主动访问源站。
九、Service Worker 与 Cache API
Service Worker 是独立于页面主线程的 Worker,可以拦截其作用域内的请求并通过 Cache API 实现可编程缓存。它通常要求安全上下文(HTTPS,localhost 开发环境例外)。
它和 HTTP 缓存不是互斥关系:Service Worker 先决定如何响应,内部可能调用 Cache API,也可能调用网络;网络请求本身仍有 HTTP 缓存。
一个版本化缓存示例
const CACHE_NAME = 'app-shell-v3'
const APP_SHELL = [
'/',
'/assets/app.8d31c2.js',
'/assets/app.4fa91e.css'
]
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(APP_SHELL))
)
})
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys
.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
))
)
})
self.addEventListener('fetch', event => {
if (event.request.method !== 'GET') return
event.respondWith(
caches.match(event.request).then(cachedResponse => {
const networkResponse = fetch(event.request).then(response => {
if (response.ok && response.type === 'basic') {
const copy = response.clone()
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, copy)
})
}
return response
})
// 这里是 cache-first;内容站也可以采用 network-first。
return cachedResponse || networkResponse
})
)
})
原文示例中缺少 fetch() 的返回值,缓存未命中时会让请求没有响应;上面补上了网络回退。真实项目要继续处理:
- 缓存失败和网络失败;
- 不缓存 404/500 和含用户数据的响应;
- Cache API 中的
Response只能消费一次,所以缓存前要clone(); - 更新提示、
skipWaiting()和clientsClaim()的版本切换风险; - 清理旧缓存、配额不足和跨源 opaque response;
- 不把 Service Worker 当成永远在线的后台进程,浏览器可以暂停或终止它。
十、Memory Cache、Disk Cache、BFCache 和 HTTP/2 Push
1. Memory/Disk Cache
DevTools 的 from memory cache 和 from disk cache 有助于观察一次测试,但具体保存在哪个位置、查找顺序和是否复用是浏览器实现细节。不能写成固定的:
Service Worker -> Memory Cache -> Disk Cache -> Push Cache
Service Worker 是可编程拦截层,不是所有浏览器都以同样顺序展示;内存和磁盘缓存还会受到资源类型、进程、浏览器版本、设备内存和缓存压力影响。不要依赖“脚本一定在内存、CSS 一定在磁盘”的结论。
2. BFCache 不是资源缓存
Back/Forward Cache 会保存可恢复的页面快照。用户点击后退/前进时,浏览器可能直接恢复页面,不重新下载 HTML,也不一定重新触发缓存验证。页面可以监听:
window.addEventListener('pageshow', event => {
if (event.persisted) {
console.log('页面从 BFCache 恢复')
}
})
unload、某些未关闭的资源、同步 API 等可能影响进入 BFCache。不要把“后退很快”简单解释成 HTTP 缓存命中。
3. HTTP/2 Push
原文把 Push Cache 作为 HTTP/2 缓存层。HTTP/2 Server Push 已被主流浏览器移除或不再推荐,不能作为现代项目的常规缓存策略。现在应使用合理的 HTML 关键路径、preload、HTTP 缓存、CDN 和 103 Early Hints(如果部署链路支持)。
prefetch 是低优先级的资源预取提示,也不是一个独立的“第五级缓存”;浏览器可以忽略它,且预取资源的复用受缓存策略影响。
十一、浏览器本地存储与离线存储
Cookie
Cookie 更适合会话标识和少量需要随请求发送的信息:
Set-Cookie: __Host-session=RANDOM_ID; Path=/; Secure; HttpOnly; SameSite=Lax
HttpOnly防止 JavaScript 读取,但不能阻止 XSS 发起已认证请求;Secure只允许通过 HTTPS 发送;SameSite控制跨站发送并帮助降低 CSRF;Domain越宽,受影响的子域越多,应谨慎设置;Path主要是发送范围,不是可靠的安全隔离边界;Max-Age是相对秒数,Expires是日期,两者都存在时通常Max-Age优先;- Cookie 会附加到请求,不能存放大 JSON、图片或密码。
localStorage
- 同源范围内跨标签页共享;
- API 同步,频繁读写大数据会阻塞主线程;
- 字符串存储,需自行序列化;
- 没有原生 TTL;
- XSS 可以读取其中的内容,因此不适合存放长期高价值 Token;
- 容量和持久性受浏览器配额、隐私模式和用户清理影响。
sessionStorage
sessionStorage 按源和顶级浏览上下文隔离。同一标签页刷新仍然存在,标签页关闭后通常释放;新窗口是否复制初始值还与打开方式有关。不要把“同源所有标签页共享”误用于 sessionStorage。
IndexedDB
IndexedDB 是异步、事务化、同源的结构化存储,适合离线数据、较大对象和队列。它不等于“无限容量”,写入必须处理 QuotaExceededError、升级阻塞、版本迁移和用户清理。
WebSQL 已废弃,不应在新项目中使用。Cache API 适合 Request/Response 资源,IndexedDB 适合业务数据,两者可以组合但职责不同。
十二、实际项目的缓存建议
静态资源
Cache-Control: public, max-age=31536000, immutable
前提是 URL 使用内容指纹,且构建产物不会被原地覆盖。
HTML 入口
Cache-Control: no-cache
ETag: "html-v42"
也可以使用较短 max-age 并配合 CDN 失效。重点是保证新 HTML 能引用新资源。
个性化 API
Cache-Control: private, no-cache
Vary: Accept-Encoding
如果数据极其敏感或不应落盘:
Cache-Control: no-store
图片和字体
- 对内容指纹图片和字体使用长缓存;
- 对不同尺寸使用
srcset,避免缓存原图后发送过多字节; - 字体更新时改变 URL,避免旧字体和 CSS 不一致;
- CDN 缓存前确认响应不含用户私密信息。
十三、刷新行为不能绝对化
开发者文章常把刷新简单归纳成 Ctrl+F5 跳过所有缓存、F5 只验证协商缓存、地址栏回车直接使用磁盘缓存。实际行为与浏览器、平台、开发者工具的 Disable cache、请求缓存模式、Service Worker 和 BFCache 有关。
更可靠的理解是:
- 正常导航按资源的缓存新鲜度和验证器处理;
- 普通 reload 通常会要求重新验证或降低复用程度;
- force reload 会更积极地绕过缓存;
- DevTools 的 Disable cache 主要影响打开 DevTools 时的网络请求;
- Service Worker 可能在 HTTP 缓存之前决定响应;
- 后退/前进可能从 BFCache 恢复页面。
不要把某次 DevTools 中的 200、304、from disk cache 结果当成所有浏览器的固定规则。
总结
- HTTP 缓存、CDN、Service Worker、Cookie、Web Storage、IndexedDB 和 BFCache 是不同机制;
Cache-Control: no-cache表示复用前验证,no-store才表示不要保存这次响应;Expires是兼容字段,现代缓存主要使用Cache-Control;ETag和Last-Modified都有价值,条件请求中If-None-Match通常优先;- HTML 入口通常短缓存或协商缓存,带内容指纹的 JS/CSS/图片/字体适合长缓存;
- CDN 要同时考虑共享缓存、缓存键、个性化数据、回源和失效;
- Service Worker 的缓存策略要有版本、回退、清理和更新机制;
- Memory/Disk Cache 的查找顺序属于浏览器实现细节,HTTP/2 Push 不应作为现代方案;
- 缓存策略必须和部署、版本更新、隐私和错误恢复一起设计。