HTTP 缓存机制定义在 HTTP 协议标准中,被现代浏览器广泛支持,同时也是一个用于提升基于 Web 的系统性能的广泛使用的工具。本文讨论如何使用 HTTP 缓存机制提升基于 Web 的系统,以及如何避免误用。同时也讨论了几种常见的应用场景。
抛开具体的技术实现,一般性来说在服务器 - 客户端模式下,缓存机制通常都是这么工作的,如图所示: 当客户端需要获取某个资源的时候,首先查看客户端本地缓存中是否存在此资源。如果缓存中不存在这个资源,则必须要从服务器端把资源拉到本地。如果存在,则还需要判断缓存的资源是否过期。如果没过期,则从缓存中直接获取资源,不需要再从服务器端获取。如果过期了,则必须从服务器端获取最新资源。还有一种很可能出现的情况是客户端不知道服务器端的资源更新了没有,即不知道缓存中的资源是否过期。这种情况下,客户端还是要向服务器端发请求的,不过在请求中附加了一些信息。这些信息用于服务器判断客户端缓存中的资源是否过期,例如最后修改日期。服务器端如果发现缓存的资源并未过期,那么只需要返回一个信号,告诉客户端从缓存获取即可,并不需要返回资源的内容。
当 Web 浏览器向服务器请求资源时(如下图所示),服务器可以在响应中包含缓存相关的 HTTP 头(HTTP Header)。这些 HTTP Header 会告诉浏览器是否以及如何缓存资源。
下面列出的是关于缓存的 HTTP Headers,它们有些是出现在响应(Response)中,有些在请求(Request)中。
HTTP Header | 出现在 |
---|---|
Cache-Control | 响应 |
Expires | 响应 |
Last-Modified | 响应 |
If-Modified-Since | 请求 |
ETag | 响应 |
If-None-Match | 请求 |
下面会一一介绍这些 Header 的用法,有一些是要配套使用的。
在 Chrome 调试器中,可以在 Response 中找到 Cache-Control,例如下图中的服务响应就包含一个 Cache-Control 头,其值为"public, max-age=30"
Cache-Control 的值一般为 [public | private | no-cache, no-store ], [max-age=n] ([A | B] 表示可以从 A 和 B 中取一个),"max-age=n"表示 n 秒后资源失效。
public 是指资源应该被缓存,并且中间经过的代理服务器(假如有的话)也应该缓存这个资源。隐含的意思是,其他用户可能也能分享这个资源的缓存。 private 是指资源应该被缓存,但是只能被客户端的浏览器缓存。 no-cache, no-store 是指示资源不应该被缓存。 max-age 是指缓存多长时间,单位是秒。
下面举几个常用的例子:
Cache-Control : public, max-age=60
告诉浏览器,当前资源应该被缓存,同时也告诉代理服务器(假如有的话)也可以缓存这个资源。缓存应该在 60 秒后过期。
Cache-Control : private, max-age=60
告诉浏览器,当前资源应该被缓存,同时也告诉代理服务器(假如有的话)不要缓存这个资源。缓存应该在 60 秒后过期。
Cache-Control : no-cache, no-store 告诉浏览器以及代理服务器,不要缓存这个资源。
是相对于 max-age 的另一种指示过期时间的方式。max-age 表示多少时间后过期,而 Expires 表示在某个日期时间点后过期。换种通俗的说法,max-age 相当于说“保质期六个月”,而 Expires 是说“在此日期之前”饮用。
注意:如果 max-age 和 Expires 同时存在,应该以 max-age 为准。但是通常可以把两个都设置成一个时间点。例如下面的 JavaScript 代码所示:
// Set the max age to 1 year.
res.setHeader('Cache-Control', 'public, max-age=31536000');
// Set expire date to 1 year later.
var currentDate = new Date((new Date()).getTime() + 3600 * 1000 * 24 * 365);
res.setHeader('Expires', currentDate.toUTCString());
限制:max-age 和 Expires 设置的缓存过期时间最多为一年(365 天),如果多于这个值则浏览器有可能会忽略。
Last-Modified 出现在响应中,告诉浏览器当前资源的最后修改时间,例如:
Last-Modified: Mon, 03 Jan 2011 17:45:57 GMT
而 If-Modified-Since 出现在下次请求中,询问服务器当前缓存的资源是否已经过期,例如:
If-Modified-Since: Mon, 03 Jan 2011 17:45:57 GMT
如果资源没有过期,则服务器应该返回 304,不需要返回资源的内容。如果已过期,则服务器应该返回资源的内容到浏览器,并且返回 200 HTTP 状态码。
Last-Modified 和 If-Modified-Since 配套使用,可以在保证不会误用缓存里的过期资源的前提下,减少服务器向浏览器发送的数据量,以及由于服务器在缓存未过期的情况下只需要返回 304 HTTP 状态码,而不需要返回整个资源的内容,也给了服务器优化的机会。
Last-Modified 和 If-Modified-Since 是用日期时间判断一个缓存的资源是否有效。ETag 和 If-None-Match 则是用内容摘要作为判定的依据。内容摘要是指为一个资源的内容产生一串比较短的数字,当内容变化时,产生的数字串也会改变。内容摘要的算法有很多种,较常见的是 SHA-1 哈希算法、CRC32 等。
ETag 包含在服务器的响应中,为内容摘要。在下一次请求中,If-None-Match 的值为上次的 ETag 的值。服务器根据 If-None-Match 的值(即内容摘要)判断缓存的资源是否有效。
304 状态码("Not Modified")表示资源(相对于缓存过的)没有被修改过。
这里仅以 Google Chrome 为例,介绍如何检查请求与响应,以及检查浏览器缓存是否在发挥作用。其他的浏览器平台也是类似的。
打开 Google Chrome,按 F12,出现调试界面,切换到 Network 页。
你会看到很多请求,包括 URL、方法、状态等等,注意到上图有一栏显示的是"(from cache)",已经用红框标出。这表示这些资源都是从缓存里获取的,没有经过服务器。清除 Cache 之后,看到的会是浏览器从服务器端获取资源,如下图所示:
点击其中的一项,显示出请求和响应的细节信息。 跟 HTTP 缓存相关的 Header 已经被红框标出,这里真正发挥作用 Cache-Control 和 Expires,这两个让浏览器根本不用发请求。浏览器只要发请求就会产生一定的延时,因为即使服务器返回 304 这样简单的数据,在底层也还需要 TCP/IP 层的握手等各种操作,而且 HTTP 协议也会有额外开销。
下面罗列了几种场景,并讨论如何设定缓存策略。
对于那些不经常改变的静态资源,比如 CSS、图片、动画等,应尽可能地利用缓存。因为这些资源通常很大而且几乎每个页面可能都会用到,缓存会大大提高系统效率。对于这些资源,响应中应该包含如下内容:
Cache-Control:public; max-age=31536000
Expires: Mon, 25 Jun 2013 21:31:12 GMT
max-age=31536000 意味着 31536000 秒(也就是一年)后缓存失效。这里尤为注意不能设置成多于一年,因为 RFC 上限制了最大只能是一年,超过一年的情况不同的浏览器处理策略不同,有些直接就忽略了 Cache-Control。
对于动态内容,需要依据内容的实际情况,定义合适的 max-age。例如对于 SNS 网络中的时间线通常可以设置成几秒。
对于需要登录才能访问到资源,Cache-Control 应该设置成 private 以禁止代理服务器缓存这些资源,否则会威胁信息安全。
某些情况下需要禁止使用缓存,则应该把 Cache-Control 设置成"no-cache, no-store",如下所示。
Cache-Control:no-cache, no-store