深入理解Web缓存:从浏览器到CDN的完整缓存机制
1. 引言
在现代Web应用中,缓存是提升性能和用户体验的关键技术。然而,缓存机制的复杂性常常让开发者感到困惑。从浏览器的本地缓存到CDN的边缘缓存,再到源服务器的应用缓存,每一层都有其独特的工作原理和配置方式。
为什么缓存如此重要
- 性能提升:减少网络请求,加快页面加载速度
- 带宽节省:减少重复的数据传输
- 服务器压力减轻:降低源服务器的负载
- 用户体验改善:更快的响应时间和更流畅的交互
常见的缓存误区和困惑
许多开发者对缓存机制存在误解,比如:
- 认为CDN只根据
max-age决定是否缓存 - 不理解ETag在CDN层的作用机制
- 混淆浏览器缓存和CDN缓存的区别
- 对条件请求的工作原理理解不够深入
本文将解决的问题
本文将通过实际测试和案例分析,深入探讨Web缓存的完整机制,帮助开发者:
- 理解多层缓存架构的交互关系
- 掌握HTTP缓存控制机制的核心原理
- 学会配置和优化CDN缓存策略
- 避免常见的缓存配置错误
2. 缓存的多层架构
Web缓存系统是一个多层架构,每一层都有其特定的作用和优化目标。
浏览器缓存
浏览器缓存是距离用户最近的缓存层,包括:
内存缓存:
- 存储在浏览器内存中
- 访问速度最快
- 页面关闭后会被清除
- 适用于当前会话的临时资源
磁盘缓存:
- 存储在本地磁盘上
- 持久化存储,浏览器重启后仍然存在
- 访问速度较内存缓存慢,但比网络请求快
- 适用于长期缓存的资源
CDN缓存
CDN(内容分发网络)缓存位于边缘节点,特点包括:
地理位置优化:
- 部署在全球各地的边缘节点
- 用户从最近的节点获取内容
- 减少网络延迟
缓存策略控制:
- 根据HTTP头部信息决定缓存策略
- 支持条件请求和缓存验证
- 可配置不同类型资源的缓存规则
源服务器缓存
源服务器缓存包括应用层缓存和数据缓存:
应用层缓存:
- 页面级缓存(如SSR页面缓存)
- 组件级缓存
- API响应缓存
数据缓存:
- 数据库查询缓存
- 会话缓存
- 业务逻辑缓存
缓存层级的交互关系
用户请求 → 浏览器缓存 → CDN缓存 → 源服务器缓存 → 数据源
每一层都可能成为缓存命中点,越靠近用户的缓存命中,性能越好。
3. HTTP缓存控制机制
HTTP协议提供了丰富的缓存控制机制,主要通过HTTP头部来实现。
3.1 Cache-Control指令详解
Cache-Control是HTTP/1.1引入的缓存控制头部,提供了精细的缓存控制能力。
max-age:缓存生存时间
1Cache-Control: max-age=3600
- 指定资源在缓存中的最大生存时间(秒)
- 3600表示缓存1小时
- 缓存过期后需要重新验证或获取
must-revalidate:缓存验证要求
1Cache-Control: max-age=3600, must-revalidate
- 缓存过期后必须向源服务器验证
- 不能使用过期的缓存内容
- 确保内容的新鲜度
no-cache vs no-store:区别与使用场景
no-cache:
1Cache-Control: no-cache
- 可以缓存,但每次使用前必须验证
- 适用于需要确保内容新鲜度的场景
- 仍然可以利用304 Not Modified优化
no-store:
1Cache-Control: no-store
- 完全不缓存,每次都重新获取
- 适用于敏感信息或实时性要求极高的内容
- 性能影响较大
private vs public:缓存范围控制
private:
1Cache-Control: private, max-age=3600
- 只能被浏览器缓存
- CDN不会缓存此内容
- 适用于用户个人信息
public:
1Cache-Control: public, max-age=3600
- 可以被所有缓存层缓存
- 包括CDN、代理服务器等
- 适用于公共资源
3.2 ETag机制深入解析
ETag(Entity Tag)是资源的唯一标识符,用于缓存验证。
什么是ETag:资源的"指纹"
1ETag: "v1.0.0-abc123"
ETag的作用:
- 为资源生成唯一标识
- 检测资源是否发生变化
- 支持条件请求
- 实现精确的缓存控制
强ETag vs 弱ETag:精确匹配与模糊匹配
强ETag:
1ETag: "v1.0.0-abc123"
- 资源的任何变化都会改变ETag
- 字节级别的精确匹配
- 适用于需要精确验证的场景
弱ETag:
1ETag: W/"v1.0.0-abc123"
- 以
W/开头 - 允许语义等价但字节不同的资源
- 适用于可接受微小差异的场景
ETag的生成策略
版本号方式:
1const etag = `"${version}"`
内容哈希方式:
1const etag = `"${crypto.createHash('md5').update(content).digest('hex')}"`
时间戳方式:
1const etag = `"${Date.now()}"`
条件请求:If-None-Match的工作原理
客户端发送条件请求:
1GET /page.html HTTP/1.1 2If-None-Match: "v1.0.0-abc123"
服务器响应:
- ETag匹配:返回304 Not Modified
- ETag不匹配:返回200 OK + 新内容
3.3 Last-Modified机制与时间戳验证
除了ETag,HTTP还提供了基于时间戳的缓存验证机制。
Last-Modified的工作原理
服务器响应:
1 2HTTP/1.1 200 OK 3 4Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT 5 6Cache-Control: max-age=3600 7
客户端条件请求:
1 2GET /page.html HTTP/1.1 3 4If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT 5
服务器响应:
-
文件未修改:返回304 Not Modified
-
文件已修改:返回200 OK + 新内容 + 新的Last-Modified
ETag vs Last-Modified:精确度对比
| 特性 | ETag | Last-Modified |
|---|---|---|
| 精确度 | 内容级别 | 时间级别 |
| 适用场景 | 内容敏感 | 时间敏感 |
| HTTP版本 | HTTP/1.1 | HTTP/1.0 |
| 优先级 | 高 | 低 |
| 性能开销 | 较高(需计算) | 较低 |
示例对比:
1 2// ETag方式 - 内容变化立即检测 3 4const etag = crypto.createHash('md5').update(content).digest('hex') 5 6res.setHeader('ETag', `"${etag}"`) 7 8 9 10// Last-Modified方式 - 基于文件修改时间 11 12const stats = fs.statSync(filepath) 13 14res.setHeader('Last-Modified', stats.mtime.toUTCString()) 15
组合使用策略
最佳实践:同时使用ETag和Last-Modified
1 2// 服务器响应 3 4res.setHeader('ETag', `"${version}"`) 5 6res.setHeader('Last-Modified', new Date().toUTCString()) 7 8res.setHeader('Cache-Control', 'max-age=3600, must-revalidate') 9 10 11 12// 客户端会发送两个验证头 13 14// If-None-Match: "v1.0" 15 16// If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT 17
验证优先级:
-
如果有ETag:优先使用ETag验证
-
如果只有Last-Modified:使用时间戳验证
-
如果两者都有:ETag匹配即可,忽略Last-Modified
3.4 协商缓存与强制缓存的区别
HTTP缓存机制可以分为两大类:强制缓存和协商缓存。
强制缓存(Force Cache)
定义:浏览器直接从缓存中获取资源,不与服务器通信。
控制头部:
1 2Cache-Control: max-age=3600 3 4Expires: Wed, 21 Oct 2015 07:28:00 GMT 5
工作流程:
1. 浏览器检查缓存
2. 缓存未过期 → 直接使用缓存
3. 缓存过期 → 进入协商缓存流程
特点:
-
🚀 性能最优:无网络请求
-
📱 节省带宽:零网络消耗
-
⚡ 响应最快:直接从本地获取
-
🔄 控制困难:更新不及时
协商缓存(Negotiated Cache)
定义:浏览器与服务器协商确定是否使用缓存。
控制头部:
1 2ETag: "v1.0" 3 4Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT 5
工作流程:
1. 浏览器发送条件请求
2. 服务器验证资源是否变化
3. 未变化 → 304 Not Modified
4. 已变化 → 200 OK + 新内容
特点:
-
🔄 更新及时:确保内容新鲜
-
🌐 需要通信:每次都要询问服务器
-
💾 节省带宽:304响应无body
-
⚖️ 平衡性能:在速度和准确性间平衡
缓存策略对比
| 缓存类型 | 网络请求 | 带宽消耗 | 更新及时性 | 适用场景 |
|---|---|---|---|---|
| 强制缓存 | 无 | 零 | 较差 | 静态资源 |
| 协商缓存 | 有 | 很少 | 很好 | 动态内容 |
实际应用场景
强制缓存场景:
1 2// 静态资源:长期缓存 3 4res.setHeader('Cache-Control', 'max-age=31536000, immutable') 5 6// 适用于:JS、CSS、图片等版本化资源 7
协商缓存场景:
1 2// 动态内容:协商缓存 3 4res.setHeader('ETag', `"${version}"`) 5 6res.setHeader('Cache-Control', 'no-cache') // 强制验证 7 8// 适用于:HTML页面、API响应、用户数据 9
混合策略:
1 2// 短期强制缓存 + 协商缓存 3 4res.setHeader('ETag', `"${version}"`) 5 6res.setHeader('Cache-Control', 'max-age=300, must-revalidate') 7 8// 5分钟内强制缓存,之后协商缓存 9
关键理解:
-
强制缓存和协商缓存不是互斥的
-
可以组合使用,形成多层缓存策略
-
现代Web应用通常采用混合策略
-
根据资源特性选择合适的缓存方式
3.5 其他重要的缓存相关头部字段
除了上述核心字段外,还有一些重要但不常见的缓存头部字段。
Expires:传统的缓存过期时间
HTTP/1.0的缓存控制:
1 2Expires: Wed, 21 Oct 2015 07:28:00 GMT 3
特点:
-
指定具体的过期时间
-
依赖客户端和服务器时钟同步
-
被Cache-Control的max-age覆盖
-
兼容性好,支持HTTP/1.0
问题:
1 2// 时区问题示例 3 4res.setHeader('Expires', 'Wed, 21 Oct 2015 07:28:00 GMT') // UTC时间 5 6// 如果客户端时区不同,可能导致计算错误 7
最佳实践:
1 2// 现代应用推荐使用Cache-Control替代Expires 3 4res.setHeader('Cache-Control', 'max-age=3600') // 相对时间,更可靠 5 6res.setHeader('Expires', new Date(Date.now() + 3600000).toUTCString()) // 兼容性备用 7
Pragma:早期的缓存控制
HTTP/1.0的no-cache指令:
1 2Pragma: no-cache 3
现代用法:
1 2// 向后兼容的no-cache设置 3 4res.setHeader('Cache-Control', 'no-cache') 5 6res.setHeader('Pragma', 'no-cache') // 兼容HTTP/1.0 7
Vary:缓存变化控制
控制缓存的变化维度:
1 2Vary: Accept-Encoding, User-Agent, Accept-Language 3
作用:
-
告诉缓存服务器根据哪些请求头部来区分缓存
-
同一个URL可能有多个缓存版本
-
防止错误的缓存匹配
实际应用:
1 2// 根据编码格式缓存不同版本 3 4res.setHeader('Vary', 'Accept-Encoding') 5 6// 支持gzip的客户端和不支持的客户端会缓存不同版本 7 8 9 10// 多语言网站 11 12res.setHeader('Vary', 'Accept-Language') 13 14// 不同语言的用户会缓存不同版本的页面 15 16 17 18// 移动端适配,这个还是挺有用的,尤其多端适配的网站 19 20res.setHeader('Vary', 'User-Agent') 21 22// 手机和PC可能缓存不同版本的页面 23
注意事项:
1 2// 过多的Vary头部会降低缓存效率 3 4res.setHeader('Vary', 'Accept-Encoding, User-Agent, Accept-Language, Cookie') 5 6// 可能导致缓存碎片化,命中率下降 7
Age:缓存年龄
表示缓存的存在时间:
1 2Age: 120 3
含义:
-
资源在缓存中存在的时间(秒)
-
由缓存服务器(如CDN)自动添加
-
用于计算缓存是否过期
计算公式:
remaining_time = max-age - age
如果 remaining_time > 0,缓存有效
如果 remaining_time <= 0,缓存过期
实际应用:
1 2// 客户端可以通过Age判断缓存新鲜度 3 4const cacheAge = parseInt(response.headers['age'] || '0') 5 6const maxAge = parseInt(response.headers['cache-control'].match(/max-age=(\d+)/)[1]) 7 8const remainingTime = maxAge - cacheAge 9 10console.log(`缓存还有 ${remainingTime} 秒过期`) 11
Cache-Control的高级指令
s-maxage:CDN专用缓存时间:
1 2Cache-Control: max-age=300, s-maxage=3600 3
-
max-age=300:浏览器缓存5分钟 -
s-maxage=3600:CDN缓存1小时 -
允许不同层级设置不同的缓存时间
proxy-revalidate:代理必须重新验证:
1 2Cache-Control: max-age=3600, proxy-revalidate 3
-
代理缓存过期后必须验证
-
浏览器缓存可以继续使用过期内容
no-transform:禁止内容转换:
1 2Cache-Control: no-transform 3
-
禁止代理服务器修改内容
-
防止压缩、格式转换等操作
stale-while-revalidate:后台更新策略:
1 2Cache-Control: max-age=3600, stale-while-revalidate=86400 3
-
缓存过期后,可以使用过期内容响应用户
-
同时在后台异步更新缓存
-
提升用户体验
stale-if-error:错误时使用过期缓存:
1 2Cache-Control: max-age=3600, stale-if-error=86400 3
-
当服务器返回错误时,使用过期缓存
-
提高服务可用性(这个也有用,防止服务挂了)
条件请求的完整头部家族
If-Match:强验证:
1 2If-Match: "v1.0", "v1.1" 3
-
只有ETag匹配时才执行请求
-
用于防止更新冲突
If-Unmodified-Since:反向时间验证:
1 2If-Unmodified-Since: Wed, 21 Oct 2015 07:28:00 GMT 3
-
只有资源未修改时才执行请求
-
与If-Modified-Since相反
If-Range:范围请求的条件验证:
1 2If-Range: "v1.0" 3 4Range: bytes=0-1023 5
-
只有ETag匹配时才返回范围内容
-
否则返回完整资源
实际配置示例
完整的缓存头部配置:
1 2// Node.js示例 3 4function setCacheHeaders(res, options = {}) { 5 6const { 7 8 maxAge = 3600, 9 10 sMaxAge = maxAge * 2, 11 12 etag = generateETag(), 13 14 lastModified = new Date(), 15 16 vary = ['Accept-Encoding'] 17 18} = options 19 20 21 22 // 基础缓存控制 23 24 res.setHeader('Cache-Control', `max-age=${maxAge}, s-maxage=${sMaxAge}, must-revalidate`) 25 26 // 验证机制 27 28 res.setHeader('ETag', etag) 29 30 res.setHeader('Last-Modified', lastModified.toUTCString()) 31 32 // 变化控制 33 34 res.setHeader('Vary', vary.join(', ')) 35 36 // 向后兼容 37 38 res.setHeader('Expires', new Date(Date.now() + maxAge * 1000).toUTCString()) 39 40 res.setHeader('Pragma', 'no-cache') 41 42 // 高级指令 43 44 res.setHeader('Cache-Control', 45 `max-age=${maxAge}, s-maxage=${sMaxAge}, must-revalidate, stale-while-revalidate=${maxAge * 24}` 46 ) 47 48} 49
CDN优化配置:
1 2// 针对CDN的特殊优化 3 4function setCDNOptimizedHeaders(res, resourceType) { 5 6 switch (resourceType) { 7 8 case 'html': 9 res.setHeader('Cache-Control', 'max-age=300, s-maxage=3600, must-revalidate') 10 res.setHeader('Vary', 'Accept-Encoding, Accept-Language') 11 break 12 13 case 'static': 14 res.setHeader('Cache-Control', 'max-age=31536000, s-maxage=31536000, immutable') 15 res.setHeader('Vary', 'Accept-Encoding') 16 break 17 18 case 'api': 19 res.setHeader('Cache-Control', 'max-age=60, s-maxage=300, must-revalidate') 20 res.setHeader('Vary', 'Accept, Authorization') 21 break 22 } 23} 24
头部字段优先级和兼容性
优先级顺序:
-
Cache-Control > Expires
-
ETag > Last-Modified
-
s-maxage > max-age(对于CDN)
-
条件请求头部按照RFC标准处理 兼容性考虑:
1 2// 确保最大兼容性的头部设置 3 4function setCompatibleCacheHeaders(res, maxAge = 3600) { 5 6 // HTTP/1.1 标准 7 8 res.setHeader('Cache-Control', `max-age=${maxAge}, must-revalidate`) 9 10 // HTTP/1.0 兼容 11 12 res.setHeader('Expires', new Date(Date.now() + maxAge * 1000).toUTCString()) 13 14 res.setHeader('Pragma', 'no-cache') 15 16 // 现代浏览器优化 17 18 res.setHeader('Cache-Control', 19 20 `max-age=${maxAge}, must-revalidate, stale-while-revalidate=${maxAge}` 21 22 ) 23 24} 25
4. CDN缓存机制实践
我们使用CloudFront的CDN以及Nuxt,来实现如下的诉求:
- HTML文件发布的时候,CDN会主动回源,获取新的版本
- 在发布下一次版本之前,所有的HTML都能够命中CDN缓存。(这种场景在于没有一些个性化推荐的时候来用)

4.1 Nuxt.js中的缓存配置
在Nuxt.js中,我们可以通过hooks配置HTTP响应头:
1// nuxt.config.js 2export default { 3 hooks: { 4 'render:beforeResponse': (url, result, context) => { 5 // 只为HTML响应设置缓存策略 6 if (result.html) { 7 // 生成基于版本的ETag 8 const etag = `"${version}"` 9 context.res.setHeader('ETag', etag) 10 11 // 设置合理的缓存时间 12 context.res.setHeader( 13 'Cache-Control', 14 'max-age=3600, must-revalidate' 15 ) 16 } 17 } 18 } 19}
4.2 验证
我们第一次打开的时候,会返回一个Etag。然后去发布一个新的版本,再次刷新页面,会返回新的页面,此时的Etag发生了变化:(可以看到请求带了If-None-Match)

再次刷新的时候,CDN就直接返回304,符合我们目前的预期:

5. 缓存策略最佳实践
5.1 不同资源类型的缓存策略
HTML文件:短期缓存 + ETag验证
策略:
1Cache-Control: max-age=3600, must-revalidate 2ETag: "v1.0.0"
原因:
- HTML文件变化频繁,需要快速更新
- ETag确保内容准确性
- 短期缓存平衡性能和新鲜度
实现:
1// 为HTML设置短期缓存 2if (result.html) { 3 context.res.setHeader('ETag', `"${version}"`) 4 context.res.setHeader('Cache-Control', 'max-age=3600, must-revalidate') 5}
静态资源:长期缓存 + 版本化文件名
策略:
1Cache-Control: max-age=31536000, immutable
原因:
- 静态资源变化少,可以长期缓存
- 版本化文件名确保更新时的正确性
immutable指令告诉浏览器文件不会改变
实现:
1// webpack配置 2module.exports = { 3 output: { 4 filename: '[name].[contenthash].js', 5 chunkFilename: '[name].[contenthash].js' 6 } 7}
API响应:按需缓存 + 条件请求
策略:
1Cache-Control: max-age=300, must-revalidate 2ETag: "data-v1.0"
原因:
- API数据变化频率不一,需要灵活控制
- 条件请求减少不必要的数据传输
- 相对较短的缓存时间保证数据新鲜度
实现:
1// API响应缓存 2app.get('/api/data', (req, res) => { 3 const data = getData() 4 const etag = generateETag(data) 5 6 res.setHeader('ETag', etag) 7 res.setHeader('Cache-Control', 'max-age=300, must-revalidate') 8 9 if (req.headers['if-none-match'] === etag) { 10 res.status(304).end() 11 } else { 12 res.json(data) 13 } 14})
总结
通过深入分析Web缓存机制,我们得出以下关键结论:
-
CDN缓存比我们想象的更智能:现代CDN如CloudFront会主动检测ETag变化,而不是简单地依赖max-age。
-
ETag是缓存验证的核心:正确使用ETag可以实现精确的缓存控制,确保用户获得最新内容。
-
缓存策略需要分层设计:不同层级的缓存应该有不同的策略,形成完整的缓存体系。
-
实际测试胜过理论分析:只有通过实际测试,我们才能真正理解缓存机制的工作原理。
掌握这些知识,可以帮助我们构建更高效、更可靠的Web应用缓存系统。
公众号关注一波~

网站源码:linxiaowu66 · 豆米的博客
Follow:linxiaowu66 · Github
关于评论和留言
如果对本文 深入理解Web缓存:从浏览器到CDN的完整缓存机制 的内容有疑问,请在下面的评论系统中留言,谢谢。