vlambda博客
学习文章列表

【联合阅读】浏览器缓存策略


前言

众所周知,在 Web 开发中,缓存很重要、很有用。但同时其也很复杂。

本文将从以下 5 个方面全面地介绍下缓存相关的内容。

  1. 缓存的判断策略
  2. 必知必会的缓存基础
  3. 各类缓存的优缺点
  4. 缓存的最佳实践
  5. 小试牛刀,看看你掌握了没有?

一、缓存的判断策略

浏览器对于所请求资源的缓存处理有一套完整的机制,主要包含以下三个策略:存储策略、过期策略、协商策略。

其中,存储策略发生在收到请求响应后,用于决定是否缓存相应资源;过期策略发生在请求前,用于判断缓存是否过期;协商策略发生在请求中,用于判断缓存资源是否更新。

浏览器在应用缓存策略时,具体的判断流程如下:

上图中的缓存判断流程是浏览器在应用缓存时完整的判断流程。但是在浏览器中访问资源的方式不同也会导致判断流程的不同。判断流程会根据不同方式跳过一些流程。

浏览器下访问资源的方式主要有以下 7 种:

  1. (新标签)地址栏回车
  2. 链接跳转
  3. 前进、后退
  4. 从收藏栏打开链接
  5. (window.open)新开窗口
  6. 刷新(Command + R / F5)
  7. 强制刷新(Command + Shift + R / Ctrl + F5)

使用这 7 种方式访问资源时,应用缓存的策略会有一些不同。如下图所示。通过上述 7 种方式访问资源,会从不同的缓存应用判断步骤开始。此处不做验证,相信大家看了后面的内容,能够自行验证的。【联合阅读】浏览器缓存策略

本文配有测试脚本,代码在github[1]上。下文会按照测试脚本进行述说,使用说明见下载链接。验证上述内容,可以执行node cache-ETag+max-age.js,会同时开启ETagmax-age,然后触发相应的动作,通过 Network 面板和 node 日志即可验证,此处篇幅有限先不赘述。

在 Chrome 下刷新时,只有主资源的缓存应用方式如上图所示,派生资源的缓存应用方式与新标签打开类似,会判断缓存是否过期。强缓存生效时的区别在于新标签打开为from disk cache,而当前页刷新派生资源是from memory cache

而在 Firefox 下,当前页面刷新,所有资源都会如上图所示。下文也会利用 Chrome 的这一特点在当前页刷新,派生资源会使用缓存进行测试。不然每次都需要打开新标签较为繁琐。

二、必知必会的缓存基础

HTTP 中与缓存有关的字段主要有以下 10 个,如下表所示。为明确表示其功能及用法,下表中分别区分了存储策略、过期策略、协商策略、请求头、响应头。

【联合阅读】浏览器缓存策略
缓存相关头字段表格

注:乄表示半对,Last-Modified之所以是半对,是因为有可能会触发启发式缓存,也会缓存文件。具体见下文。

缓存又分为强缓存和弱缓存(又称为协商缓存)。其中强缓存包括ExpiresCache-Control,主要是在过期策略生效时应用的缓存。弱缓存包括Last-ModifiedETag,是在协商策略后应用的缓存。强弱缓存之间的主要区别在于获取资源时是否会发送请求。

2.1 Expires

如上所述,Expires指定缓存的过期时间,为绝对时间,即某一时刻。参考本地时间进行比对,在指定时刻后过期。RFC 2616[2]建议最大值不要超过 1 年。

Expire头字段是响应头字段,格式如下:Expires: Sat Oct 20 2018 00:00:00 GMT+0800 (CST)

可以尝试以下步骤进行验证:

  1. 执行 node cache-Expires.js,该脚本会给请求的资源设定 Expires,值为:"2018-10-20 00:00:00"。
  2. 访问地址 http://localhost:1030/,开启 Network Tab,查看 avatar.jpg 图片,Expires 值如下所示。 【联合阅读】浏览器缓存策略
  3. 再次刷新会看到该资源已经被缓存,size 栏显示为 (from memory cache)。此时修改本地时间,将时间修改为“2018-10-15 00:00:00”,再刷新,会发现缓存仍然有效。 【联合阅读】浏览器缓存策略
  4. 如果将本地时间修改为“2018-10-25 00:00:00”,再刷新,会发现图片不再使用缓存,而是重新获取了,因为本地时间超过了设定值。 【联合阅读】浏览器缓存策略

2.2 Cache-Control

Cache-Control用于指定资源的缓存机制,可以同时在请求头和响应头中设定,涉及上述三个策略中的两个策略:存储策略、过期策略。

Cache-Control的语法如下:Cache-Control: cache-directive[,cache-directive]cache-directive为缓存指令,大小写不敏感,共有 12 个与 HTTP 缓存标准相关,如下表所示。其中请求指令 7 种,响应指令 9 种。Cache-Control可以设置多个缓存指令,以逗号,分隔。

【联合阅读】浏览器缓存策略
Cache-Control 指令表

2.3.1 cache-directive 大小写不敏感

如上,cache-directive 指令大小写不敏感,所以在设置 Cache-Control 时,指令可以不区分大小写。不过建议统一使用小写。验证如下:

  1. 执行 node cache-directive-case-insensitive.js,会服务端会将 max-age写成大写,如下 Cache-Control: MAX-AGE=86400
  2. 再次请求浏览器会发现缓存同样会生效。

2.3.2 在请求头中的 max-age

max-age 在请求头中的主要应用为max-age=0表示不使用缓存。Chrome 和 Firefox 浏览器下的刷新操作(Command+ R / F5)均是在请求头上添加了max-age=0指令,表示不使用强缓存,但允许协商缓存(在介绍了协商缓存的Last-ModifiedETag之后,可以自行验证下这一点)。

刷新时Cache-Controlmax-age=0验证如下:

  1. 单独访问图片资源 http://localhost:1030/avatar.jpg,开启 Network
  2. 刷新,可在响应头中看到上述内容。如下图所示。(Firefox 下相同,不单独验证,主要最开始提到的主资源和派生资源在两个浏览器中表现形式的不同)。 【联合阅读】浏览器缓存策略

此外,经验证,Chrome 和 Firefox 均对max-age>0 的情况支持不好。

  1. 在 Chrome 下,通过 Modify Headers插件(Chrome 和 Firefox 下均有类似插件)给请求添加 max-age=7200
  2. 执行 node cache-max-age.js,访问 http://localhost:1030,先强刷保证资源更新。
  3. 打开 NetWork,查看 avatar.jpg,刷新,会发现,资源访问仍然走的是缓存。如果按照规范的定义应该是不生效。 【联合阅读】浏览器缓存策略

2.3.3 max-age 与 Expires

Cache-Control 中的max-age指令用于指定缓存过期的相对时间。资源达到指定时间后过期。该功能与 Expires 类似。但其优先级高于 Expires,如果同时设置 max-age 和 Expires,max-age 生效,忽略 Expires。验证如下:

  1. 执行 node cache-max-age+Expires.js,会同时设置 Cache-Control: max-age=86400 / Expires: Mon Oct 20 2018 00:00:00 GMT+0800 (CST),如下所示。 【联合阅读】浏览器缓存策略
  2. 刷新,然后再把本地时间改成当前时间延后 2 小时(不超过 20 号),会发现缓存生效。(以下两步不再附截图,与上述示例类似)。
  3. 如果将时间改为两天后(假设 20 号离现在大于两天,否则结果相反),会发现缓存不再生效,因为超出了 max-age 的限制。

相反,可以再试一下,max-age 的有效时间大于 Expires 的情况,会发现依然是 max-age 生效。

2.3.4 no-cache 和 no-store

还有一点需要注意的是,no-cache 并不是指不缓存文件,no-store 才是指不缓存文件。no-cache 仅仅是表明跳过强缓存,强制进入协商策略。

2.3 Pragma

http1.0 字段, 通常设置为Pragma:no-cache, 作用与Cache-Control:no-cache相同。当在浏览器进行强刷(Comand + Shift + R / Ctrl + F5)或在 NetWork 面板内勾选禁用缓存(Disable Caches)时,会自动带上Pragma:no-cacheCache-Control:no-cache并且不会带上协商策略中所涉及的信息(下面介绍的If-Modified-Since/If-None-Match)。这是不会使用任何缓存,重新获取资源。如下图所示。

【联合阅读】浏览器缓存策略
强刷浏览器自动设置no-cache

2.4 Last-Modified/If-Modified-Since/If-Unmodified-Since

Last-Modified用于标记请求资源的最后一次修改时间。语法格式为:Last-Modified: <day-name>,<day> <month> <year> <hour>:<minute>:<second> GMT,即 GMT(格林尼治标准时间)。可用 new Date().toGMTString()获取当前 GMT 时间。由于 Last-Modified 只能精确到秒,因此不适合在一秒内多次改变的资源。

如果 Expires,Cache-Control: max-age,或 Cache-Control:s-maxage 都没有在响应头中出现,并且设置了Last-Modified时,那么浏览器默认会采用一个启发式的算法,即启发式缓存。通常会取响应头的 Date_value - Last-Modified_value 值的 10%作为缓存时间。验证如下:

  1. 执行 node cache-Last-Modified.js,服务器会获取资源的最后修改时间,设置为 Last-Modified的值。访问 localhost:1030,查看 avatar.jpg,如下图所示: 【联合阅读】浏览器缓存策略
  2. 刷新浏览器,会发现图片会从缓存获取。
  3. 通过启发式缓存的公司可以计算出缓存的时间,修改本地时间超过缓存时间后,再刷新,会发现缓存失效。

2.4.1 If-Modified-Since

返回的资源带有Last-Modified标识时,再次请求该资源,浏览器会自动带上If-Modified-Since,值为返回的Last-Modified值。请求到达服务器后,服务器进行判断,如果从上次更新后没有再更新,则返回 304。如果更新了则重新返回。验证如下:

  1. 执行 node cache-Last-Modified.js,服务器会获取资源的最后修改时间,设置为 Last-Modified的值。如下图所示,并且注意看一下资源的大小。 【联合阅读】浏览器缓存策略 【联合阅读】浏览器缓存策略
  2. 刷新页面,再次查看 NetWork。会发现请求头中带上了 If-Modified-Since。如果服务器判断资源未改变,则返回 304,此外由于服务器返回 304,资源会从缓存获取,所以资源大小也减少了,如下所示。 【联合阅读】浏览器缓存策略 【联合阅读】浏览器缓存策略
  3. 修改 index.html文件的内容,再次刷新。会发现返回变成 200,html 内容更新了,并且返回了新的 Last-Modified的值,资源大小也相应地改变了。 【联合阅读】浏览器缓存策略 【联合阅读】浏览器缓存策略

304 请求也可以触发存储策略,如文章开头的流程判断图所示,可自行验证,返回时添加相应 header 即可。

注意,If-Modified-Since只能用于 GET、HEAD 请求。

2.4.2 If-Unmodified-Since

If-Unmodified-Since表示资源未修改则正常执行更新,否则返回 412(Precondition Failed)状态码的响应。主要有如下两种场景。

  1. 用于不安全的请求中从而是请求具备条件性(如 POST 或者其他不安全的方法),如请求更新 wiki 文档,文档未修改时才执行更新。
  2. If-Range字段同时使用时,可以用来保证新的片段请求来自一个未修改的文档。

2.5 ETag/If-Match/If-None-Match

ETag 是请求资源在服务器的唯一标识,浏览器可以根据 ETag 值缓存数据。在再次请求时通过If-None-Match携带上次的 ETag 值,如果值不变,则返回 304,如果改变你则返回新的内容。

需要注意的是,ETag 和 If-None-Match 的值均为双引号包裹的。

验证步骤与Last-Modified相似。执行node cache-ETag.js即可。此处不再详述。

If-Match判断逻辑逻辑与If-None-Match相反。

最后,ETag的优先级高于Last-Modified。当ETagLast-ModifiedETag优先级更高,但不会忽略Last-Modified,需要服务端实现。验证如下,其中服务端判断优先级:

  1. 执行 node cache-ETag+Last-Modified.js。服务端会在资源的响应头中,同时设置 ETagLast-Modified。如下图: 【联合阅读】浏览器缓存策略
  2. 刷新浏览器,会发现 index.html请求时 304。查看 node 日志,会看到 ETag生效。如下: 【联合阅读】浏览器缓存策略

三、缓存的优缺点

好了,通过长长的第二部分,我们简单介绍了一下 HTTP Cache 的基础知识。下面我再汇总一下各类缓存之间的优缺点吧。如下表所示:

【联合阅读】浏览器缓存策略
缓存优缺点表

四、最佳实践

从上面各类缓存的优缺点可以看出,每一种缓存都不是完美的。所以建议像下面这样做:

  1. 不要缓存 HTML,避免缓存后用户无法及时获取到更新内容。
  2. 使用 Cache-ControlETag来控制 HTML 中所使用的静态资源的缓存。一般是将 Cache-Controlmax-age设成一个比较大的值,然后用 ETag进行验证。
  3. 使用签名或者版本来区分静态资源。这样静态资源会生成不同的资源访问链接,不会产生修改之后无法感知的情况。

还有两个本文没有介绍的内容,但是不建议大家使用:

  1. 使用 HTML 的 meta 标签来指定缓存行为
  2. 使用查询字符串来避免缓存。因为缓存有一些 已知的问题 [3],使用查询字符串会导致有些代理服务器不缓存资源。

五、小试牛刀,看看你掌握了没有?

看了这么多内容,是时候来看看成果了。那么一起看下下面的问题吧。

如果首次访问localhost:1030时,页面中 avatar.png 响应头信息如下:

HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Type: image/png
Last-Modified: Tue, 16 Oct 2018 11:42:28 GMT
Accept-Ranges: bytes
Date: Tue, 16 Oct 2018 15:57:21 GMT

问题 1:请问当刷新该页面后,avatar.png 如何二次加载?

问题 2:如果将上述信息中的Cache-Control设置为 private,那么结果又会如何呢?

大家先回忆下上面的内容,思考一下。

试题来源:。在此致谢。

好了公布答案。

问题 1:会带着If-Modified-Since和服务端进行验证。未改变返回 304,改变返回 200。

问题 2:Cache-Control设置为 private,这时候会触发启发式缓存,则再次刷新时,avatar.png 命中强缓存,从缓存中换取。

总结

好了,文章到此结束,希望能对大家有帮助。

致谢

感谢《深入浅出 Vue.js》作者刘博文[4]对本文提出的宝贵建议。

参考链接

  1. MDN | Cache-Control
  2. 由 memoryCache 和 diskCache 产生的浏览器缓存机制的思考 [5]
  3. A Web Developer’s Guide to Browser Caching [6]
  4. 浏览器缓存机制剖析 [7]
  5. HTTP 缓存 [8]
  6. Are Your Cache-Control Directives Doing What They Are Supposed to Do? [9]
  7. Hypertext Transfer Protocol [10]

文内链接

[1]

github: https://github.com/verymuch/learning-web-cache

[2]

RFC 2616: https://tools.ietf.org/html/rfc2616

[3]

已知的问题: https://gtmetrix.com/remove-query-strings-from-static-resources.html

[4]

刘博文: https://github.com/berwin

[5]

由 memoryCache 和 diskCache 产生的浏览器缓存机制的思考: https://segmentfault.com/a/1190000011286027

[6]

A Web Developer’s Guide to Browser Caching: https://medium.com/@codebyamir/a-web-developers-guide-to-browser-caching-cc41f3b73e7c

[7]

浏览器缓存机制剖析: http://louiszhai.github.io/2017/04/07/http-cache/

[8]

HTTP 缓存: https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching

[9]

Are Your Cache-Control Directives Doing What They Are Supposed to Do?: https://techblog.thescore.com/2014/11/19/are-your-cache-control-directives-doing-what-they-are-supposed-to-do/

[10]

Hypertext Transfer Protocol: https://tools.ietf.org/html/rfc7234#section-5.2.1




扫描二维码

获取更多精彩

歪码行空

    



最后让我知道你在看吧