深入理解Web缓存:从浏览器到CDN的完整缓存机制

发表于 2025-11-29
更新于 2025-11-29
分类于 技术专栏
阅读量 6
字数统计 13269

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:精确度对比

特性ETagLast-Modified
精确度内容级别时间级别
适用场景内容敏感时间敏感
HTTP版本HTTP/1.1HTTP/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

验证优先级

  1. 如果有ETag:优先使用ETag验证

  2. 如果只有Last-Modified:使用时间戳验证

  3. 如果两者都有: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

头部字段优先级和兼容性

优先级顺序

  1. Cache-Control > Expires

  2. ETag > Last-Modified

  3. s-maxage > max-age(对于CDN)

  4. 条件请求头部按照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缓存。(这种场景在于没有一些个性化推荐的时候来用)

image.png

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)

image.png

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

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缓存机制,我们得出以下关键结论:

  1. CDN缓存比我们想象的更智能:现代CDN如CloudFront会主动检测ETag变化,而不是简单地依赖max-age。

  2. ETag是缓存验证的核心:正确使用ETag可以实现精确的缓存控制,确保用户获得最新内容。

  3. 缓存策略需要分层设计:不同层级的缓存应该有不同的策略,形成完整的缓存体系。

  4. 实际测试胜过理论分析:只有通过实际测试,我们才能真正理解缓存机制的工作原理。

掌握这些知识,可以帮助我们构建更高效、更可靠的Web应用缓存系统。

公众号关注一波~

微信公众号

关于评论和留言

如果对本文 深入理解Web缓存:从浏览器到CDN的完整缓存机制 的内容有疑问,请在下面的评论系统中留言,谢谢。

网站源码:linxiaowu66 · 豆米的博客

Follow:linxiaowu66 · Github