浅谈HTTP和CDN缓存控制的那点事

HTTP 的缓存机制,可以说这是前端工程师需要掌握的重要知识点之一。本文将针对 HTTP 缓存整体的流程做一个详细的讲解,争取做到大家读完整篇文章后,对缓存有一个整体的了解。

HTTP 缓存分为 2 种,一种是强缓存,另一种是协商缓存。主要作用是可以加快资源获取速度,提升用户体验,减少网络传输,缓解服务端的压力。这是缓存运作的一个整体流程图:

浏览器缓存

浏览器缓存又分为 Disk Cache(存放在硬盘中)和 Memory Cache(存放在内存中),存放的位置是由浏览器控制的。是否浏览器缓存由 Expires、Cache-Control 和 Pragma 3 个 Header 属性共同来控制。

Expires

Expires 是一个HTTP响应头,用于指定响应的过期日期和时间。它是HTTP/1.0中的缓存机制,用来告知客户端(通常是浏览器)在何时之前可以将响应视为新鲜的,而无需再次请求服务器。

在浏览器发起请求时,会根据系统时间和 Expires 的值进行比较,如果系统时间超过了 Expires 的值,缓存失效。由于和系统时间进行比较,所以当系统时间和服务器时间不一致的时候,会有缓存有效期不准的问题。Expires 的优先级在三个 Header 属性中是最低的。

用法说明

  1. 格式:
    • Expires 的值是一个HTTP日期格式的字符串,表示响应被视为过期的时间点。
    • 格式示例:Expires: Wed, 21 Oct 2023 07:28:00 GMT
  2. 工作原理:
    • 当浏览器接收到带有 Expires 头的响应时,它会将该时间与当前日期时间进行比较。
    • 如果当前时间早于 Expires 指定的时间,浏览器将认为该响应是新鲜的,可以直接从缓存中使用,而无需向服务器发起新的请求。
    • 如果当前时间已经超过 Expires 指定的时间,响应被视为过期,浏览器通常会发起新请求去获取最新的资源。
  3. 优缺点:
    • 优点:简单明了,易于理解和实现。
    • 缺点:Expires 使用绝对时间,可能会受到客户端和服务器时钟不同步的影响。此外,Expires 无法动态调整缓存策略,因为日期是固定的。
  4. 与 Cache-Control 的关系:
    • 在HTTP/1.1中,Cache-Control 被引入以提供更灵活和强大的缓存控制机制。
    • Cache-Control: max-ageExpires 同时存在时,Cache-Control: max-age 会覆盖 Expires
    • Expires 主要用于向后兼容旧的HTTP/1.0客户端。

Pragma

Pragma 是一个HTTP/1.0协议中的请求头和响应头,主要用于控制缓存行为。在现代Web开发中,它的使用已经非常有限,因为HTTP/1.1引入了更强大和灵活的Cache-Control头来代替它。然而,了解Pragma的用法仍然有助于理解一些旧系统或确保向后兼容。

用法说明

  1. Pragma: no-cache:
    • 最常见的用法是Pragma: no-cache,用于请求头中。
    • 它指示中间缓存服务器(如代理)和客户端浏览器不应缓存请求的响应,甚至不应使用缓存中的响应。
    • 在HTTP/1.0中,这种方式用于强制客户端每次从服务器获取最新的资源。
  2. 限制和注意事项:
    • Pragma头没有在HTTP/1.1中正式定义为响应头的一部分,因此在响应中使用没有标准化的效果。
    • 在HTTP/1.1中,Cache-Control: no-cache已经成为更为标准和推荐的方式。
    • Pragma: no-cache通常与Cache-Control: no-cache一起使用,以确保兼容HTTP/1.0和HTTP/1.1客户端。
  3. 现代用法:
    • 由于Pragma主要是为HTTP/1.0设计的,现在很少在新项目中单独使用。
    • 如果需要确保兼容性和想要强制不缓存,开发者通常会同时设置Cache-ControlPragma

Cache-Control

Cache - Control是 HTTP/1.1 中用于控制缓存的头部字段。它提供了比传统的Expires头更灵活的缓存控制机制。客户端(如浏览器)和中间缓存服务器(如代理服务器)可以根据Cache - Control的值来决定是否使用缓存的响应以及缓存的时长等。

Cache-Control常用响应头如下:

字段名 说明
public 包含public指令的响应资源表示允许被任何中间者(可能是代理服务器、类似于 cdn 网络)缓存。这个指令通常不需要在响应头中用到,因为其他指令已经表明了响应资源是否可以被缓存(例如:max-age)。
private private 指令表示响应资源仅仅只能被获取它的浏览器端缓存。它不允许任何中间者(intermediate)缓存响应的资源。
no-cache no-chache 使用 ETag 响应头来告知客户端(浏览器、代理服务器)这个资源首先需要被检查是否在服务端修改过,在这之前不能被复用。这个意味着no-cache将会和服务器进行一次通讯,确保返回的资源没有修改过,如果没有修改过,才没有必要下载这个资源。反之,则需要重新下载。
no-store no-store 在处理资源不能被缓存和复用的逻辑的时候与 no-cache 类似,然而,他们之间有一个重要的区别。no-store要求资源每次都被请求并且下载下来。当在处理隐私信息(private information)的时候,这是一个重要的特性。
max-age=[秒] 这个指令告诉浏览器端或者中间者,响应资源能够在它被请求之后的多长时间以内被复用。例如,max-age等于 3600 意味着响应资源能够在接下来的 60 分钟以内被复用,而不需要从服务端重新获取。(可以发现,max-age的单位是秒)
s-maxage=[秒] s-maxage 与上文提到的max-age类似,这里的“s”代表共享,并且,这个指令一般仅用于CDNs或者其他中间者(intermediary caches)。这个指令会覆盖max-ageexpires响应头。
must-revalidate 一旦缓存过期,必须向服务器重新验证缓存的有效性。与no - cache不同的是,must - revalidate在缓存未过期时可以正常使用缓存,而no - cache每次使用缓存前都要验证。例如,对于一些重要的配置文件的缓存,服务器可以设置Cache - Control: max - age = 600; must - revalidate。在 10 分钟(600 秒)内,浏览器可以使用缓存的配置文件,一旦超过 10 分钟,浏览器必须向服务器验证缓存的配置文件是否仍然有效,确保使用的是最新的配置。
proxy-revalidate * 与must-revalidate指令类似,proxy-revalidate也强调重新验证缓存。不过,must-revalidate是应用于所有缓存(包括客户端浏览器缓存和代理服务器缓存),而proxy-revalidate主要是针对代理服务器缓存。
immutable * 以一个大型静态资源库(如 JavaScript 库或 CSS 样式库)为例。这些资源通常很少发生变化,并且版本更新时往往是通过更改文件名或路径来实现的。假设一个网站引用了一个稳定版本的jQuery库,服务器可以在响应头中设置Cache - Control: immutable, max - age = 31536000(缓存有效期为 1 年)。浏览器在首次获取这个jQuery库后,由于immutable指令的存在,在 1 年的时间内,它将不会向服务器发送任何请求来验证这个库是否更新,而是直接使用缓存中的内容。例如,在一个有代理服务器的网络环境中,对于某些内容(如一些更新频率较低的新闻资讯网页),服务器可能希望代理服务器在缓存过期后重新验证,以确保代理服务器提供给后续客户端的内容是最新的。这时可以使用proxy - revalidate指令。
stale-while-revalidate=[秒] 当缓存内容过期后,正常情况下缓存系统需要先向服务器验证缓存是否有效才能将内容返回给客户端。但是有了stale-while-revalidate指令,缓存系统会先把过期的内容提供给客户端,然后再进行验证。例如,设置Cache-Control: stale-while-revalidate = 60,这意味着当缓存过期后,缓存系统可以在接下来的 60 秒内,先把过期的内容返回给客户端,同时在后台向服务器发送验证请求。
stale-if-error=[秒] 正常情况下,缓存系统会按照Cache - Control中的其他指令(如max - age)来判断缓存是否过期。当缓存过期后,会尝试重新验证缓存或者获取新内容。然而,如果在这个过程中发生错误,stale - if - error指令就会起作用。例如,设置Cache - Control: stale - if - error = 3600,这意味着如果在缓存过期后,重新验证或者获取新内容出现错误,缓存系统可以在接下来的 1 小时(3600 秒)内提供过期的缓存内容给客户端。

Cache-Control控制流程图

协商缓存

当浏览器的强缓存失效的时候或者请求头中设置了不走强缓存,并且在请求头中设置了 If-Modified-Since 或者 If-None-Match 的时候,会将这两个属性值到服务端去验证是否命中协商缓存,如果命中了协商缓存,会返回 304 状态,加载浏览器缓存,并且响应头会设置 Last-Modified 或者 ETag 属性。

ETag/If-None-Match

ETag/If-None-Match 的值是一串 hash 码,代表的是一个资源的标识符,当服务端的文件变化的时候,它的 hash 码会随之改变,通过请求头中的 If-None-Match 和当前文件的 hash 值进行比较,如果相等则表示命中协商缓存。ETag 又有强弱校验之分,如果 hash 码是以 “W/” 开头的一串字符串,说明此时协商缓存的校验是弱校验的,只有服务器上的文件差异(根据 ETag 计算方式来决定)达到能够触发 hash 值后缀变化的时候,才会真正地请求资源,否则返回 304 并加载浏览器缓存。

常见使用场景如下:

  • 静态资源缓存
    • 对于一些静态资源,如图片、CSS 文件、JavaScript 文件,服务器可以使用ETagIf-None-Match进行缓存验证。
    • 当客户端请求这些资源时,先从缓存中获取ETag,并在后续请求中通过If-None-Match发送给服务器。如果资源未修改,客户端可避免重新下载,加快页面加载速度。
  • 动态资源缓存
    • 对于动态生成的资源,如根据用户信息生成的页面,也可以使用ETag。服务器可以根据用户信息的变化或数据更新生成不同的ETag
    • 当用户信息或数据更新时,ETag会变化,客户端重新获取更新后的资源;未更新时,可继续使用缓存。

Last-Modified/If-Modified-Since

Last-Modified/If-Modified-Since 的值代表的是文件的最后修改时间,第一次请求服务端会把资源的最后修改时间放到 Last-Modified 响应头中,第二次发起请求的时候,请求头会带上上一次响应头中的 Last-Modified 的时间,并放到 If-Modified-Since 请求头属性中,服务端根据文件最后一次修改时间和 If-Modified-Since 的值进行比较,如果相等,返回 304 ,并加载浏览器缓存。

常见使用场景如下:

  • 静态资源管理
    • 对于静态资源,如图片、CSS 或 JavaScript 文件,使用 Last-ModifiedIf-Modified-Since 可以有效地管理缓存。
    • 当客户端首次请求资源时,服务器在响应中包含 Last-Modified 时间,之后客户端再次请求时,通过 If-Modified-Since 传递该时间,帮助服务器判断是否需要发送新资源。
  • 动态资源处理
    • 对于动态生成的资源,服务器可以根据数据更新的时间设置 Last-Modified。例如,一个博客文章页面,当文章更新时,更新 Last-Modified 时间。
    • 客户端会在后续请求中使用 If-Modified-Since 来检查文章是否更新,避免重新下载未修改的页面,提高性能。