HTTP 缓存
介绍
HTTP 缓存是 Web 性能优化中至关重要的一个概念。当客户端(如浏览器)向服务器发出请求时,服务器会返回一个响应,这个响应可以被缓存并存储。下次当客户端发出相同请求时,缓存可以直接提供先前存储的响应,而不必再向源服务器发起请求。
HTTP 缓存减少了重复的网络请求和服务器负载,提高了整体的响应速度。
缓存类型
私有缓存 (Private Cache)
私有缓存是与特定客户端绑定的缓存,由于存储的响应不与其他客户端共享,因此私有缓存可以存储该用户的个性化响应。
浏览器缓存是最常见的缓存类型,浏览器会将资源(如 HTML、CSS、JavaScript、图片等)缓存到本地,以便在用户再次访问时快速加载。
- 工作原理:当用户请求某个资源时,浏览器会检查该资源是否已经缓存,并且缓存是否仍然有效。如果有效,浏览器将直接从缓存中获取资源;如果无效,浏览器会向服务器发起请求,并更新缓存。
- 缓存控制:浏览器缓存主要依赖 Cache-Control、Expires、ETag 和 Last-Modified 等 HTTP 头来控制缓存的有效期和更新策略。
如果响应包含个性化内容,并且您只想将响应存储在私有缓存中,则必须指定 private 指令。
Cache-Control: private
共享缓存 (Shared Cache)
共享缓存位于客户端和服务器之间,可以存储可在用户之间共享的响应。
共享缓存一般位于客户端和服务器之间的代理服务器或内容分发网络(CDN)上。它们缓存响应数据,减少对源服务器的请求。
- 工作原理:当用户请求某个资源时,代理服务器会检查缓存中是否已经有该资源。如果有,它会直接从缓存中返回资源;如果没有,它会将请求转发给源服务器获取数据并缓存。
- 缓存控制:代理缓存遵循与浏览器缓存类似的缓存头部,如 Cache-Control、Expires 和 Vary,但通常会处理大量的缓存数据并根据网络条件决定是否缓存。
启发式缓存
HTTP 旨在尽可能多地缓存,因此即使没有给出 Cache-Control 或 Expires 等头部,如果满足某些条件,响应也会被存储和重用。这称为启发式缓存 。
启发式缓存通过使用一些预设的规则(主要基于资源的 最后修改时间 Last-Modified 和 当前时间),为这些响应确定一个合理的缓存时间,从而在没有完整缓存信息的情况下依然能提高性能。
虽然启发式缓存在很多情况下都能够提高性能,但在可能的情况下,建议开发者通过适当的缓存头(如 Cache-Control、Expires 和 ETag)来明确指定缓存策略,以确保缓存的准确性和一致性。
缓存过期
缓存的过期主要是由响应头中的 max-age 和 Age 决定的。
- max-age:这个响应头指定缓存响应的最大存活时间。也就是在这个时间内缓存是新鲜的,可以被直接重用。
- Age:这个响应头标示了缓存已经存储的时间,也就是说它表明从响应生成到当前时间已经过去的时间。
通过这两个标头,缓存判断响应是否仍然有效。如果 Age 小于 max-age,则响应是 fresh(新鲜的),可以直接用来响应客户端请求;如果 Age 大于 max-age,响应就会变成 stale(过时的),此时缓存需要重新验证或重新获取数据。
在 HTTP/1.0 中,缓存过期是通过 Expires 标头来指定的,该标头使用一个明确的时间戳来指示缓存的过期时间。然而,由于 Expires 标头的时间格式难以解析,且在实现过程中发现了许多问题,比如系统时钟的恶意调整。
因此在 HTTP/1.1 中,引入了 Cache-Control: max-age 来替代 Expires,现在已不再需要使用 Expires 标头。
缓存的KEY:Vary
Vary 标头用于指定哪些请求头会影响响应的内容,从而使缓存系统根据这些请求头来分别缓存不同版本的响应。
通常,响应是基于请求的 URL 来区分的,但在很多情况下,相同的 URL 可能会根据不同的请求头(如 Accept、Accept-Language、Accept-Encoding、User-Agent 等)返回不同的内容。为了避免缓存同一个 URL 时的混淆,可以使用 Vary 标头指示缓存系统,哪些请求头的变化应该导致缓存的不同版本。
例如:
Vary: Accept-Language
在某些情况下,开发者可能会考虑根据 User-Agent(用户代理)来优化响应内容,比如为不同设备(手机、平板、桌面)提供不同的响应。然而,User-Agent 请求头有很多变化,特别是随着浏览器和设备的增多,这会导致缓存变得非常分散,极大地降低缓存重用的效果。
缓存重新验证
过时的响应不会立即被丢弃。HTTP 提供了一种机制,允许通过向源服务器发送请求来将过时的响应转变为新的响应,这个过程称为 验证(或有时称为 重新验证)。
验证通常通过发送条件请求来实现,该请求包含 If-Modified-Since 或 If-None-Match 请求头。通过这些请求头,客户端可以向服务器询问资源是否已发生变化,从而决定是否需要重新获取更新的响应。
Last-Modified / If-Modified-Since
以下响应是在 22:22:22 生成的, 最大有效期为 1 小时,因此您知道它在 23:22:22 之前是最新的。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600<!doctype html>
在 23:22:22 时,响应变得过时,并且缓存无法重复使用。因此,下面的请求显示了一个客户端发送带有 If-Modified-Since 请求标头的请求,以询问服务器自指定时间以来是否进行了任何更改。
GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-Modified-Since: Tue, 22 Feb 2022 22:00:00 GMT
如果内容自指定时间以来未更改,服务器将响应 304 Not Modified。由于此响应仅表示 “no change”,因此没有响应正文(只有一个状态代码)因此传输大小非常小。
收到该响应后,客户端会将存储的过时响应恢复为最新响应,即在剩余的 1 小时内重复使用它。
ETag / If-None-Match
ETag 响应头的值是服务器生成的任意值。服务器必须如何生成值没有限制,因此服务器可以根据它们选择的任何方式(例如正文内容的哈希值或版本号)自由设置值。
例如,如果 ETag 标头使用哈希值,并且 index.html 资源的哈希值为 33a64df5,则响应将如下所示:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
ETag: "33a64df5"
Cache-Control: max-age=3600<!doctype html>
如果该响应过时,则客户端将获取缓存响应的 ETag 响应标头的值,并将其放入 If-None-Match 请求标头中,以询问服务器资源是否已被修改:
GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-None-Match: "33a64df5"
如果服务器为请求的资源确定的 ETag 标头的值与请求中的 If-None-Match 值相同,则服务器将返回 304 Not Modified。
但是,如果服务器确定请求的资源现在应该具有不同的 ETag 值,则服务器将改为使用 200 OK 和最新版本的资源进行响应。
强制重新验证(Force Revalidation)
如果您希望始终从服务器获取最新的内容,而不是重用缓存中的响应,可以使用 Cache-Control: no-cache 指令来强制进行验证。该指令确保每次请求时都要求服务器检查资源是否发生变化,从而决定是否提供更新的内容。
具体实现方法是,在响应中添加 Cache-Control: no-cache,并结合 Last-Modified 和 ETag 头部。当客户端请求的资源已更新时,服务器返回 200 OK 响应;如果资源未更新,则返回 304 Not Modified 响应,表明客户端可以继续使用缓存的内容。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
ETag: "deadbeef"
Cache-Control: no-cache<!doctype html>
不缓存
在 HTTP 中,no-cache 和 no-store 是两种用于控制缓存行为的重要指令。它们虽然都与缓存相关,但有不同的使用场景和效果。
no-cache
no-cache 指令的作用是防止浏览器直接使用缓存的响应,而是强制客户端在每次使用缓存前都需要向服务器发送验证请求。也就是说,客户端会检查缓存中的响应是否已过时,如果响应已被更新,服务器将返回最新的内容。如果缓存中的响应未过期,则客户端会使用它。
Cache-Control: no-cache
作用:强制每次请求都进行验证,确保服务器内容是最新的。
应用场景:适用于需要在缓存中存储响应内容,但又需要确保内容是否被更新的情况。
注意事项:no-cache 不会阻止缓存存储响应,只是防止缓存的内容在没有重新验证的情况下被使用。
no-store
no-store 是一个更为严格的指令,用来防止响应被存储在任何缓存中。使用该指令时,服务器返回的内容不会被客户端或任何中间缓存存储,因此每次请求都会直接向服务器获取最新的资源。
Cache-Control: no-store
作用:完全阻止响应被存储在缓存中。
应用场景:适用于敏感数据(如登录信息、金融交易等)或希望始终提供最新内容的场合。
注意事项:no-store 会完全阻止缓存存储,因此会增加服务器的负担,因为每次都需要请求服务器获取数据。
存在过时的响应,no-store 不会删除已存储的旧响应,因此需要额外注意响应的更新机制。
参考
HTTP caching - HTTP | MDN