浏览器缓存是节省用户流量,提升加载效率的常用方法;但同时它也会带来获取到历史脏数据等风险,今天我们就来详细介绍下浏览器缓存相关内容。
分类
浏览器的缓存主要包括两种缓存:强缓存、验证缓存。
1. 强缓存
强缓存是指浏览器不与服务器进行任何交互请求,直接将浏览器的缓存数据(包括缓存数据的 Response 头信息)返回给用户。这种缓存给用户的响应是最快的,但同时也是风险性较高的。因为该类缓存没有进行任何的校验即直接反馈给用户,是可能存在有历史的脏数据。当浏览器的请求出现同时以下两个现象时该次请求就是强缓存:
- 浏览器返回200 (From Cache):
- 请求 Response 头中的 Date 字段所表示的时间小于当前时间:
强缓存主要是受 Cache-Control:max-age 和 Expires 头两个 Http 响应头控制的。 Cache-Control 头和 Expires 头都是都是缓存数据的有效期的信息。只不过 HTTP/1.0+ 的 Expires 头是采用的绝对 GMT 时间,而 HTTP/1.1 的 Cache-Control:max-age 则是采用的相对时间进行存储。在实际使用中我们更倾向于使用 Cache-Control:max-age 头,因为 Expires 头记录的是服务器端设置的绝对时间;如果客户端与服务器之间的时间差别较大的话可能会导致有偏差;并且当 Cache-Control:max-age 和 Expires 头同时存在的情况下, Cache-Control 头将覆盖 Expires 头。当请求发起的时间仍然在 Cache-Control 或者 Expires 设置的有效期内的话则将直接读取缓存数据。
读取强缓存的数据将是最快响应数据的方法,因为该次请求没有产生任何实际的公网访问,而仅仅是获取本地的数据即可。
2. 验证缓存
验证缓存(又叫协商缓存)是指浏览器根据缓存资源的 Last-Modified 字段和 Etag 字段得到 If-Modified-Since 和 If-None-Match 字段加入 Request 头中向服务器进行验证源站服务器的资源是否有更新过,如果服务器端收到该请求并且发现服务器端资源没有进行变更即会返回 304 Not Modified 响应头。
下面分别介绍这几个字段的意义:
Last-Modified / If-Modified-Since :在客户端第一次向服务器端发起请求时,服务器返回数据并置状态码为200,同时将该文件最后修改的 GMT 时间记录在 Last-Modified 头中返回客户端。下次客户端请求验证缓存数据时就会将缓存数据中的 Last-Modified 字段记录为请求头中的 If-Modified-Since 字段向服务器端询问在该时间点后服务器的文件是否有做过更新,如果没有更新即返回 304 Not Modified 响应头并读取缓存数据,而如果服务器文件该时间点后更新过则需要重新将服务器的文件传输给客户端并返回200状态码,同时该文件的 Last-Modified 时间也将是服务器文件现在更新的时间。下图就是一个客户端发送 If-Modified-Since 请求头给服务器端,然后服务器端验证完成后返回304给客户端。
Etag / If-None-Match : HTTP 协议规格说明定义 ETag 为“被请求变量的实体值”。另一种说法是, ETag 是一个可以与 Web 资源关联的记号(token)。 HTTP/1.1 并没有要求具体 ETag 中间需要存放什么内容或者实现方法,有一些 ETag 是通过文件资源的 MD5 值来进行标识的。与 Last-Modified 一样也是判断服务器文件是否有做过更新,其也是将上次缓存数据中的 ETag 记录为请求头中的 If-None-Match 头向服务器验证服务器文件的 ETag 是否更新过。 ETag 验证主要解决以下几点 Last-Modified 无法解决的问题:
- 网站文件周期性更新但并不改变文件内容(仅修改 Last-Modified 时间),对于这些文件仍然希望可以使用缓存数据;
- 网站文件更新频率较快,小于秒级的更新频率通过 Last-Modified 无法识别;
- 服务器无法准确得到 Last-Modified 时间,需要 ETag 标识文件。
下图就是根据 If-None-Match 验证服务器端的 ETag 后返回304的示例:
从上面的描述中可以查看到 304 是将本地缓存的 Last-Modified 和 ETag 与服务器端进行校验,因此校验缓存相比于强缓存不会出现读取本地脏数据的情况,而校验缓存的请求时间相比于强缓存较慢,而相比于完整获取服务器端的文件是较小的。因为校验缓存仍然是需要发请求到服务器端,但是 304 响应内容数据较小,因此比直接获取源文件更高效。
浏览器行为
不仅仅服务器端的 ETag 或者 Last-Modified 头会影响浏览器缓存策略,同时浏览器本身的请求头也同样会影响缓存策略。例如:浏览器有多种刷新行为可以影响下次请求对缓存数据的行为,并且不同的浏览器对于同样的刷新操作也有不同的情况。究其根本原因都是浏览器在发起请求的时候所带的 Catch-Control 的头信息来决定的。下面是 HTTP/1.1 文档中 13.2.6 Disambiguating Multiple Responses 的一段文档描述了该问题 [1]:
When a client tries to revalidate a cache entry, and the response it receives contains a Date header that appears to be older than the one for the existing entry, then the client SHOULD repeat the request unconditionally, and include
Cache-Control: max-age=0
to force any intermediate caches to validate their copies directly with the origin server, or
Cache-Control: no-cache
to force any intermediate caches to obtain a new copy from the origin server.
从上面该文档可以知晓:如果浏览器想忽略强缓存的数据而直接获取验证缓存的数据的话是需要在请求头中加上 Cache-Control:max-age=0;而浏览器如果想忽略强缓存和鉴权缓存,直接获取源服务器的内容而缓存数据就需要在请求头中加上 Cache-Control:no-cache 的头。下面是在 Chrome 下测试的结果图:
浏览器强缓存
浏览器验证缓存
浏览器不缓存
上面的测试分别是通过在地址栏回车重新键入地址、 F5 刷新以及 Ctrl+F5 刷新的测试结果。从上面的测试结果图中可以查看到与上述的结论一致;并且对于 Cache-Control: no-cache 的测试中为了兼容 HTTP/1.0 加上了 Pragma:no-cache 的头信息,其作用于前者是一致的。因此不同的浏览器对于不同的刷新操作有不同的处理逻辑也是由 Request 头中的 Cache-Control 头决定的。
流程总结
下面将根据两张图 [2] [3] 总结浏览器缓存的处理逻辑。
(1)当浏览器向服务器端发送请求的时候首先查看本地浏览器缓存数据中是否有缓存数据,如果没有缓存数据则会向 Web 服务器(这里的 Web 服务器是广义的概念,有可能并不是源站服务器,有可能是 CDN 等缓存数据)请求对应的数据并将获取的响应数据以及一些对应的 Response 头信息缓存到本地(包括 Expires 、Cache-Control 等头信息),如果有缓存数据则执行(2);
(2)当本地有缓存数据并且 Request 头中没有设置 Cache-Control:no-cache 和 Cache-Control:max-age=0 头信息的话就需要查看该缓存的 Cache-Control 头和 Expires 头查看该缓存数据是否新鲜,如果没有过期则直接读取强缓存数据返回给浏览器,返回状态码 200(From Cache) 。如果有设置上述的两个 Cache-Control 头或者强缓存数据已经过期则执行(3);
(3)如果请求头中没有 Cache-Control:no-cache 头信息的话则客户端带着 If-Modified-Since 和 If-None-Match 参数向服务器发起验证,如果服务器端验证发现没有进行更新的话则直接返回 304 Not Modified 头和本地的缓存数据返回给客户端。如果请求头带了 Cache-Control:no-cache 或者源站做了更新则执行(4);
(4)如果请求头中有 Cache-Control:no-cache 或者验证缓存校验发现源站数据更新了,则需要从服务器重新获取数据并将其存入浏览器缓存并返回200状态码。
常见问题
经常遇到如下问题均可能是浏览器缓存导致的问题,请大家注意留意:
- 添加CDN 加速源站后,客户端访问出现历史脏数据。由于添加 CDN 后浏览器到服务器端可能出现缓存的就是浏览器缓存、CDN 缓存,因此需要清楚两处的缓存后测试是否正常,如果仍然获取到脏数据有可能出现劫持的情况导致的,用户可以通过查看响应头中是否有出现 301 或者 302 的状态码查看。
- CDN 可以设置响应 HTTP 头中的 Cache-Control 和 Expires 头,这两个头与 HTTP 标准协议一致的设置方法: Cache-Control 可以设置相对时间,而 Expires 仅能够设置绝对 GMT 时间。在 CDN 上设置的 Cache-Control 和 Expires 头将仅影响浏览器缓存,并不影响 CDN 缓存策略。 CDN 的缓存策略需要源站的 Cache-Control 和 Expires 头决定。
- 当源站响应头中设置了如下的缓存策略 CDN 和浏览器都将认为是源站不允许缓存而不进行缓存:
- Cache-Control为no-cache,no-store,private
- Cache-Control为max-age=0
- Pragma为no-cache
Reference
[1] Hypertext Transfer Protocol -- HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
[2] HTTP 权威指南
[3] 缓存系列(1)——浏览器缓存协商: http://blog.csdn.net/chosen0ne/article/details/7344189
[4] http协商缓存VS强缓存: http://www.cnblogs.com/wonyun/p/5524617.html
[5] stackoverflow讨论帖: http://stackoverflow.com/questions/1046966/whats-the-difference-between-cache-control-max-age-0-and-no-cache