HTTP缓存

HTTP缓存机制及缓存策略决策
本文介绍了HTTP缓存技术,它能提升网站性能与用户体验。HTTP缓存分为强制缓存和协商缓存,强制缓存通过expires和cache - control控制,协商缓存用last - modified和ETag判断。同时指出了last - modified和ETag的不足,并给出缓存决策树来制定缓存策略。

HTTP缓存

在任何一个前端项目中,访问服务器获取数据都是很常见的事情,但是如果相同的数据被重复请求了不止一次,那么多余的请求次数必然会浪费网络带宽,以及延迟浏览器渲染所要处理的内容,从而影响用户的使用体验。如果用户使用的是按量计费的方式访问网络,那么多余的请求还会隐性地增加用户的网络流量资费。因此考虑使用缓存技术对已获取的资源进行重用,是一种提升网站性能与用户体验的有效策略。

缓存的原理是在首次请求后保存一份请求资源的响应副本,当用户再次发起相同请求后,如果判断缓存命中了这个拦截请求,就会将之前存储的响应副本返回给用户,从而避免重新向服务器发起资源请求。

缓存的技术种类有很多,比如代理缓存、浏览器缓存、网关缓存、负载均衡器等内容,它们大致可以分为两类:共享缓存和私有缓存。共享缓存指的是缓存内容可被多个用户使用,如公司内部架设的Web代理;私有缓存指的是只能单独被用户使用的缓存,如浏览器缓存。

HTTP 缓存应该算是前端开发中最常接触的缓存机制之一,它又可细分为强制缓存、协商缓存,二者最大的区别在于判断缓存命中时,浏览器是否需要向服务器端进行询问以协商缓存的相关信息,进而判断是否需要就响应内容进行重新请求。

强制缓存

对于强制缓存而言,如果浏览器判断所请求的目标资源有效命中,则可直接从强制缓存中返回请求响应,无须与服务器进行任何通信。

access-control-allow-origin: *
age: 734978
content-length: 40830
content-type: image/jpeg
cache-control: max-age=31536000
expires: Web, 14 Fed 2021 12:23:42 GMT

其中与强制缓存相关的两个字段是 expires 和 cache-control,expires 是在 HTTP 1.0 协议中声明的用来控制缓存失效日期时间戳的字段,它由服务器端指定后通过响应头告知浏览器,浏览器在接收到带有该字段的响应体后进行缓存。

若之后浏览器再次发起相同的资源请求,便会对比 expires 与本地当前的时间戳,如果当前请求的本地时间戳小于 expires 的值,则说明浏览器缓存的响应还未过期,可以直接使用而无须向服务器端再次发起请求。只有当本地时间戳大于 expires 值发生缓存过期时,才允许重新向服务器发起请求。

从上述强制缓存是否过期的判断机制有个缺点,如果客户端本地的时间与服务器端的时间不同步,或者对客户端时间进行主动修改,那么对于缓存过期的判断可能就无法和预期相符。

为了解决 expires 判断的局限性,从 HTTP 1.1 协议开始新增了 cache-control 字段来对 expires 的功能进行扩展和完善。从上述代码中可见 cache-control 设置了 maxage=31536000 的属性值来控制响应资源的有效期,它是一个以秒为单位的时间长度,表示该资源在被请求到后的 31536000 秒内有效,如此便可避免服务器端和客户端时间戳不同步而造成的问题。除此之外,cache-control 还可配置一些其他属性值来更准确地控制缓存。

no-cache 和 no-store

设置 并非像字面上的意思不使用缓存,其表示为强制进行协商缓存(后面会说),即对于每次发起的请求都不会再去判断强制缓存是否过期,而是直接与服务器协商来验证缓存的有效性,若缓存未过期,则会使用本地缓存。设置 no-store 则表示禁止使用任何缓存策略,客户端的每次请求都需要服务器端给予全新的响应。no-cache 和 no-store 是两个互斥的属性值,不能同时设置。

发送如下响应头可以关闭缓存。

Cache-Control: no-store

指定 no-cache 或 max-age=0 表示客户端可以缓存资源,每次使用缓存资源前都必须重新验证其有效性。这意味着每次都会发起 HTTP 请求,但当缓存内容仍有效时可以跳过 HTTP 响应体的下载。

Cache-Control: max-age=0

Cache-Control: no-store

###private 和 public

private 和 public 也是 cache-control 的一组互斥属性值,它们用以明确响应资源是否可被代理服务器进行缓存。

  • 若资源响应头中的 cache-control 字段设置了 public 属性值,则表示响应资源既可以被浏览器缓存,又可以被代理服务器缓存。
  • private 则限制了响应资源只能被浏览器缓存,若未显式指定则默认值为 private。

对于应用程序中不会改变的文件,你通常可以在发送响应头前添加积极缓存。这包括例如由应用程序提供的静态文件,例如图像,CSS 文件和 JavaScript 文件。

Cache-Control:public, max-age=31536000

max-age 和 s-maxage

max-age 属性值会比 s-maxage 更常用,它表示服务器端告知客户端浏览器响应资源的过期时长。在一般项目的使用场景中基本够用,对于大型架构的项目通常会涉及使用各种代理服务器的情况,这就需要考虑缓存在代理服务器上的有效性问题。这便是 s-maxage 存在的意义,它表示缓存在代理服务器中的过期时长,且仅当设置了 public 属性值时才有效。

由此可见 cache-control 能作为 expires 的完全替代方案,并且拥有其所不具备的一些缓存控制特性,在项目实践中使用它就足够了,目前 expires 还存在的唯一理由是考虑可用性方面的向下兼容。

协商缓存

协商缓存就是在使用本地缓存之前,需要向服务器端发起一次 GET 请求,与之协商当前浏览器保存的本地缓存是否已经过期。

通常是采用所请求资源最近一次的修改时间戳来判断的。

Request URL: http://localhost:3000/image.jpg
Request Method: GET

last-modified: Thu, 29 Apr 2021 03:09:28 GMT
cache-control: no-cache

当我们刷新网页时,由于该 JavaScript 文件使用的是协商缓存,客户端浏览器无法确定本地缓存是否过期,所以需要向服务器发送一次 GET 请求,进行缓存有效性的协商,此次 GET 请求的请求头中需要包含一个 ifmodified-since 字段,其值正是上次响应头中 last-modified 的字段值。

当服务器收到该请求后便会对比请求资源当前的修改时间戳与 if-modified-since 字段的值,如果二者相同则说明缓存未过期,可继续使用本地缓存,否则服务器重新返回全新的文件资源,简略截取请求头与响应头的关键信息如下:

// 再次请求的请求头
Request URL: http://localhost:3000/image.jpg
Request Method: GET
If-Modified-Since: Thu, 29 Apr 2021 03:09:28 GMT


// 协商缓存有效的响应头
Status Code: 304 Not Modified

这里需要注意的是,协商缓存判断缓存有效的响应状态码是 304,即缓存有效响应重定向到本地缓存上。这和强制缓存有所不同,强制缓存若有效,则再次请求的响应状态码是 200。

last-modifed 的不足

通过 last-modified 所实现的协商缓存能够满足大部分的使用场景,但也存在两个比较明显的缺陷:

  • 首先它只是根据资源最后的修改时间戳进行判断的,虽然请求的文件资源进行了编辑,但内容并没有发生任何变化,时间戳也会更新,从而导致协商缓存时关于有效性的判断验证为失效,需要重新进行完整的资源请求。这无疑会造成网络带宽资源的浪费,以及延长用户获取到目标资源的时间。
  • 其次标识文件资源修改的时间戳单位是秒,如果文件修改的速度非常快,假设在几百毫秒内完成,那么上述通过时间戳的方式来验证缓存的有效性,是无法识别出该次文件资源的更新的。

其实造成上述两种缺陷的原因相同,就是服务器无法仅依据资源修改的时间戳来识别出真正的更新,进而导致重新发起了请求,该重新请求却使用了缓存的 Bug 场景。

基于 ETag 的协商缓存

为了弥补通过时间戳判断的不足,从 HTTP 1.1 规范开始新增了一个 ETag 的头信息,即实体标签(Entity Tag)。

其内容主要是服务器为不同资源进行哈希运算所生成的一个字符串,该字符串类似于文件指纹,只要文件内容编码存在差异,对应的 ETag 标签值就会不同,因此可以使用 ETag 对文件资源进行更精准的变化感知。

ETag 的不足

不像强制缓存中 cache-control 可以完全替代 expires 的功能,在协商缓存中,ETag 并非 last-modified 的替代方案而是一种补充方案,因为它依旧存在一些弊端。

  • 一方面服务器对于生成文件资源的 ETag 需要付出额外的计算开销,如果资源的尺寸较大,数量较多且修改比较频繁,那么生成 ETag 的过程就会影响服务器的性能。
  • 另一方面 ETag 字段值的生成分为强验证和弱验证,强验证根据资源内容进行生成,能够保证每个字节都相同;弱验证则根据资源的部分属性值来生成,生成速度快但无法确保每个字节都相同,并且在服务器集群场景下,也会因为不够准确而降低协商缓存有效性验证的成功率,所以恰当的方式是根据具体的资源使用场景选择恰当的缓存校验方式。

缓存决策

前面我们较为详细地介绍了浏览器 HTTP 缓存的配置与验证细节,下面思考一下如何应用 HTTP 缓存技术来提升网站的性能。假设在不考虑客户端缓存容量与服务器算力的理想情况下,我们当然希望客户端浏览器上的缓存触发率尽可能高,留存时间尽可能长,同时还要 ETag 实现当资源更新时进行高效的重新验证。

但实际情况往往是容量与算力都有限,因此就需要制定合适的缓存策略,来利用有限的资源达到最优的性能效果。明确能力的边界,力求在边界内做到最好。

缓存决策树

在面对一个具体的缓存需求时,到底该如何制定缓存策略呢?我们可以参照图所示的决策树来逐步确定对一个资源具体的缓存策略。

img

首先根据资源内容的属性判断是否需要使用缓存,如果不希望对该资源开启缓存(比如涉及用户的一些敏感信息),则可直接设置 cache-control 的属性值为 no-store 来禁止任何缓存策略,这样请求和响应的信息就都不会被存储在对方及中间代理的磁盘系统上。

如果希望使用缓存,那么接下来就需要确定对缓存有效性的判断是否要与服务器进行协商,若需要与服务器协商则可为 cache-control 字段增加 no-cache 属性值,来强制启用协商缓存。

否则接下来考虑是否允许中间代理服务器缓存该资源,参考之前在强制缓存中介绍的内容,可通过为 cache-control 字段添加 private 或 public 来进行控制。如果之前未设置 no-cache 启用协商缓存,那么接下来可设置强制缓存的过期时间,即为 cache-control 字段配置 max-age=… 的属性值,最后如果启用了协商缓存,则可进一步设置请求资源的 last-modified 和 ETag 实体标签等参数。

这里建议你能够根据该决策树的流程去设置缓存策略,这样不但会让指定的策略有很高的可行性,而且对于理解缓存过程中的各个知识点也非常有帮助。

代码示例

const http = require('http')
const fs = require('fs')
const url = require('url')
const etag = require('etag')

http.createServer((req, res) => {
  console.log(req.method, req.url)

  const { pathname } = url.parse(req.url)
  if (pathname === '/') {
    const data = fs.readFileSync('./index.html')
    res.end(data)
  } else if (pathname === '/img/01.jpg') {
    const data = fs.readFileSync('./img/01.jpg')
    res.writeHead(200, {
      // 缺点:客户端时间和服务器时间可能不同步
      Expires: new Date('2021-5-27 21:40').toUTCString()
    })
    res.end(data)
  } else if (pathname === '/img/02.jpg') {
    const data = fs.readFileSync('./img/02.jpg')
    res.writeHead(200, {
      'Cache-Control': 'max-age=5' // 滑动时间,单位是秒
    })
    res.end(data)
  } else if (pathname === '/img/03.jpg') {
    const { mtime } = fs.statSync('./img/03.jpg')

    const ifModifiedSince = req.headers['if-modified-since']

    if (ifModifiedSince === mtime.toUTCString()) {
      // 缓存生效
      res.statusCode = 304
      res.end()
      return
    }

    const data = fs.readFileSync('./img/03.jpg')

    // 告诉客户端该资源要使用协商缓存
    //   客户端使用缓存数据之前问一下服务器缓存有效吗
    //   服务端:
    //     有效:返回 304 ,客户端使用本地缓存资源
    //     无效:直接返回新的资源数据,客户端直接使用
    res.setHeader('Cache-Control', 'no-cache')
    // 服务端要下发一个字段告诉客户端这个资源的更新时间
    res.setHeader('last-modified', mtime.toUTCString())
    res.end(data)
  } else if (pathname === '/img/04.jpg') {
    const data = fs.readFileSync('./img/04.jpg')
    // 基于文件内容生成一个唯一的密码戳
    const etagContent = etag(data)

    const ifNoneMatch = req.headers['if-none-match']

    if (ifNoneMatch === etagContent) {
      res.statusCode = 304
      res.end()
      return
    }

    // 告诉客户端要进行协商缓存
    res.setHeader('Cache-Control', 'no-cache')
    // 把该资源的内容密码戳发给客户端
    res.setHeader('etag', etagContent)
    res.end(data)
  } else {
    res.statusCode = 404
    res.end()
  }
}).listen(3000, () => {
  console.log('http://localhost:3000')
})
<think>好的,用户现在想了解HTTP缓存的工作原理以及前端CDN的缓存解决方案。之前我提供了关于前端CDN缓存的详细内容,现在需要结合用户的新问题,整合HTTP缓存机制,并参考用户提供的引用内容。 首先,我需要回顾HTTP缓存的基本概念,包括强制缓存协商缓存。用户提供的引用[1]中提到,使用Cache-Control头来控制缓存,比如no-store、no-cache等,还提到Nginx配置禁用HTML缓存的方法。这可能涉及到不同资源类型的缓存策略,比如HTML文件通常不缓存或使用协商缓存,而静态资源如图片、JS、CSS可以长期缓存。 接着,用户提到CDN缓存解决方案,之前回答中已经涉及了CDN的缓存流程、优化策略、多CDN加速等。需要将HTTP缓存与CDN缓存结合起来,解释它们如何协同工作。例如,CDN边缘节点会遵循源站的缓存头设置,如Cache-ControlExpires,同时CDN自身也有缓存策略,比如回源条件缓存失效机制。 用户引用的内容还提到了缓存命中率的问题,引用[1]指出Private可能影响CDN缓存命中率,但量不大时可以禁用。这可能涉及到缓存头的正确配置,避免不必要的私有缓存导致CDN无法有效缓存资源。需要解释不同Cache-Control指令对CDN的影响,比如public、private、no-cache等。 另外,用户引用中的Nginx配置示例展示了如何为HTML文件设置不缓存,这需要在前端部署时注意,确保入口文件如HTML使用协商缓存或强制不缓存,而带版本号的静态资源使用长期缓存。结合之前回答中的版本控制技巧,比如Webpack生成带哈希的文件名,可以确保资源更新后URL变化,从而绕过缓存。 用户可能还想了解如何解决常见的HTTP缓存问题,比如缓存更新不及时,或者缓存策略配置错误导致的问题。例如,使用版本化文件名来避免缓存问题,或者在需要立即更新时通过CDN刷新或修改URL。引用[1]中提到的解决方案是通过修改URL触发缓存失效,这与之前回答中的建议一致。 还需要考虑CDN如何加速动态内容,引用[2]提到CDN的动态内容回源,可能需要结合边缘计算或动态压缩技术。之前的回答中提到了动态压缩的公式,可以再次强调这些优化手段。 最后,用户的问题可能涉及实际应用中的最佳实践,比如如何配置HTTPCDN策略,监控缓存命中率,处理安全性性能的平衡。需要整合引用中的信息,如HTTPS强制、防盗链设置,以及监控指标如缓存命中率、TTFB等,确保回答全面且实用。 总结来说,需要将HTTP缓存机制与CDN缓存方案结合起来,详细解释两者的工作原理、配置策略、常见问题及解决方案,并参考用户提供的引用内容,确保回答准确且符合用户的实际需求。</think>### HTTP 缓存机制与前端 CDN 缓存解决方案 --- #### 一、HTTP 缓存核心原理 HTTP 缓存通过**客户端与服务器的协作**实现资源复用,主要分为两类: 1. **强制缓存** - 通过 `Cache-Control` 或 `Expires` 头直接判断缓存是否有效 - **典型配置**: ```http Cache-Control: public, max-age=86400 // 缓存1天 ``` - **触发条件**: - 时间未超过 `max-age` - 资源未发生版本变更(如文件名哈希未变化) 2. **协商缓存** - 通过校验文件指纹(`ETag`/`Last-Modified`)确认资源是否更新 - **典型流程**: ```mermaid graph LR A[客户端请求] --> B{本地缓存} B -->|过期| C[携带If-None-Match/If-Modified-Since请求头] C --> D{服务器校验} D -->|未修改| E[返回304状态码] D -->|已修改| F[返回新资源+新缓存头] ``` 3. **关键头字段对比** | **指令** | 作用场景 | 对 CDN 的影响 | |-------------------|----------------------------|----------------------------------------| | `public` | 允许所有节点缓存 | CDN 边缘节点可缓存资源 | | `private` | 仅允许浏览器缓存 | CDN 无法缓存,显著降低命中率[^1] | | `no-cache` | 需向服务器验证 | CDN 回源校验,增加延迟但保证内容新鲜度 | | `no-store` | 完全禁用缓存 | CDN 浏览器均不缓存 | --- #### 二、CDN 缓存HTTP 缓存的协同 CDN 本质是**分布式 HTTP 缓存代理**,其行为依赖源站配置: 1. **缓存继承规则** - CDN 节点默认遵循源站的 `Cache-Control` `Expires` 头 - **示例冲突处理**: - 若源站设置 `Cache-Control: private`,CDN 将拒绝缓存 - 若源站设置 `Cache-Control: public, max-age=3600`,CDN 缓存1小时 2. **缓存分层架构** ```mermaid graph TD A[用户] --> B[CDN边缘节点] B -->|缓存命中| C[直接返回] B -->|缓存未命中| D[CDN父节点] D -->|缓存未命中| E[源服务器] E --> D[回源并缓存] D --> B[逐层下发] ``` 3. **前端最佳实践** - **入口文件(HTML)** ```nginx # Nginx 配置示例:禁止缓存 HTML[^1] location ~* \.(html|htm)$ { add_header Cache-Control "no-cache, must-revalidate"; } ``` - **版本化静态资源(JS/CSS)** ```html <!-- Webpack 生成的带哈希文件名 --> <script src="https://cdn.com/app.3a7b2c8d.js"></script> ``` ```http Cache-Control: public, max-age=31536000 # 1年长期缓存 ``` --- #### 三、常见问题解决方案 1. **缓存更新失效** - **场景**:修改了 JS 文件但用户仍访问旧版本 - **根因**:未更新 URL 或缓存头配置错误 - **修复方案**: - 文件名添加内容哈希(如 `main.[contenthash:8].js`) - 主动刷新 CDN 缓存(需调用 CDN API) 2. **CDN 命中率低** - **排查步骤**: 1. 检查源站 `Cache-Control` 是否包含 `private` 或 `no-store`[^1] 2. 确认 CDN 配置未覆盖源站缓存头 3. 分析用户请求是否过于分散(如大量随机 URL 参数) 3. **动态 API 加速** - **CDN 边缘计算方案**: - 缓存高频查询结果(如商品详情页) - 使用 `Cache-Control: s-maxage=60` 控制 CDN 缓存时间 $$ T_{响应} = \frac{命中次数}{总请求次数} \times T_{CDN} + \frac{未命中次数}{总请求次数} \times (T_{CDN} + T_{回源}) $$ --- #### 四、进阶优化策略 1. **分级缓存策略** | **资源类型** | 浏览器缓存 | CDN 缓存 | 实现方式 | |-------------------|------------------|------------------|----------------------------------| | 入口 HTML | `no-cache` | 不缓存 | Nginx 规则匹配[^1] | | 版本化 JS/CSS | 1年 | 1年 | Webpack 哈希输出 + CDN 继承头 | | 用户个性化内容 | `private, 60s` | 不缓存 | Vary: Cookie 头 | 2. **缓存监控体系** - **核心指标**: - 浏览器缓存利用率(通过 `PerformanceResourceTiming` API 采集) - CDN 命中率(需从 CDN 服务商获取日志) - **告警阈值**: - CDN 命中率 < 85% → 触发优化任务 - HTML 文件缓存率 > 5% → 检查配置错误 --- #### 五、实践案例 **某新闻网站优化前后对比** | **问题** | 优化前现象 | 优化方案 | 结果 | |-------------------------|------------------------------|----------------------------------|------------------------------| | 首页加载慢 | JS/CSS 未缓存,重复下载 | 添加文件哈希 + 长期缓存头 | 资源加载时间减少 68% | | CDN 费用过高 | 图片请求回源率 45% | 调整 `Cache-Control: max-age=604800` | 回源率降至 12%,月节省 \$8k | | 用户看到旧页面 | HTML 被 CDN 缓存 | 强制 `no-cache` + 版本化部署 | 页面更新延迟从 2h 缩短至 10s | --- ### 相关问题 1. 如何通过 `Vary` 头实现个性化内容缓存? 2. `s-maxage` 与 `max-age` 在 CDN 场景下的区别? 3. 如何设计多环境(测试/生产)的 CDN 缓存策略? 4. Service Worker 如何与 HTTP 缓存协同工作?[^1] --- : 引用自站内资料:浏览器缓存机制及 Nginx 配置方案 [^2]: 引用自站内资料:CDN 加速流程与缓存分层架构
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值