今天看奇舞团推了篇文章讲缓存策略的,讲的挺不错,记录一下。 原文地址就在下面。
总结:
- 缓存分为强缓存和协商缓存。其中强缓存包括
Expires
和Cache-Control
,主要是在过期策略生效时应用的缓存。弱缓存包括Last-Modified
和ETag
,是在协商策略后应用的缓存。强弱缓存之间的主要区别在于获取资源时是否会发送请求。 -
Cache-Control
中的max-age
指令用于指定缓存过期的相对时间,优先级高于Expires
。Cache-Control
指定为no-cache
时,由于no-cache
相当于max-age:0,must-revalidate
,所以都存在时优先级也是高于Expires
。 -
no-cache
并不是指不缓存文件,no-store
才是指不缓存文件。no-cache
仅仅是表明跳过强缓存,强制进入协商策略。 - 如果
Expires
,Cache-Control: max-age
,或Cache-Control:s-maxage
都没有在响应头中出现,并且设置了Last-Modified
时,那么浏览器默认会采用一个启发式的算法,即启发式缓存。通常会取响应头的Date_value - Last-Modified_value值的10%作为缓存时间
前言
众所周知,在Web开发中,缓存很重要、很有用。但同时其也很复杂。
本文将从以下5个方面全面地介绍下缓存相关的内容。
- 缓存的判断策略
- 必知必会的缓存基础
- 各类缓存的优缺点
- 缓存的最佳实践
- 小试牛刀,看看你掌握了没有?
一、缓存的判断策略
浏览器对于所请求资源的缓存处理有一套完整的机制,主要包含以下三个策略:存储策略、过期策略、协商策略。
其中,存储策略发生在收到请求响应后,用于决定是否缓存相应资源;过期策略发生在请求前,用于判断缓存是否过期;协商策略发生在请求中,用于判断缓存资源是否更新。
浏览器缓存判断策略上图中的缓存判断流程是浏览器在应用缓存时完整的判断流程。但是在浏览器中访问资源的方式不同也会导致判断流程的不同。判断流程会根据不同方式跳过一些流程。
浏览器下访问资源的方式主要有以下7种:
- (新标签)地址栏回车
- 链接跳转
- 前进、后退
- 从收藏栏打开链接
- (window.open)新开窗口
- 刷新(Command + R / F5)
- 强制刷新(Command + Shift + R / Ctrl + F5)
使用这7种方式访问资源时,应用缓存的策略会有一些不同。
需要注意的是,除此之外,还有一种特殊情况。即在当前地址栏,不改变内容,直接回车,等同于刷新当前页。但是在当前页点击跳转到自身,和链接跳转一致,并不会等同于刷新。
如下图所示。通过上述8种方式访问资源,会从不同的缓存应用判断步骤开始。此处不做验证,相信大家看了后面的内容,能够自行验证的。
本文配有测试脚本,代码在github上。下文会按照测试脚本进行述说,使用说明见下载链接。验证上述内容,可以执行
node cache-ETag+max-age.js
,会同时开启ETag
和max-age
,然后触发相应的动作,通过Network面板和node日志即可验证,此处篇幅有限先不赘述。
此外,这里提一个概念,webkit资源分为主资源和派生资源。主资源是地址栏输入的URL请求返回的资源,派生资源是主资源中所引用的JS、CSS、图片等资源。
在Chrome下刷新时,只有主资源的缓存应用方式如上图所示,派生资源的缓存应用方式与新标签打开类似,会判断缓存是否过期。强缓存生效时的区别在于新标签打开为from disk cache
,而当前页刷新派生资源是from memory cache
。
而在Firefox下,当前页面刷新,所有资源都会如上图所示。下文也会利用Chrome的这一特点在当前页刷新,派生资源会使用缓存进行测试。不然每次都需要打开新标签较为繁琐。
二、必知必会的缓存基础
HTTP中与缓存有关的字段主要有以下10个,如下表所示。为明确表示其功能及用法,下表中分别区分了存储策略、过期策略、协商策略、请求头、响应头。
Key | 描述 | 存储策略 | 过期策略 | 协商策略 | 请求头 | 响应头 |
---|---|---|---|---|---|---|
Expires | 指定缓存的过期时间,值为某一时刻(绝对时间)。在指定时刻后过期 | ✓ | ✓ | ✓ | ||
Cache-Control | 指定缓存机制 | ✓ | ✓ | ✓ | ✓ | |
Pragma | 指定缓存机制(http1.0字段) | ✓ | ||||
Last-Modified | 资源最后修改时间 | 乄 | ✓ | ✓ | ||
If-Modified-Since | 缓存协商校验字段,为上次请求收到的Last-Modified的值。处理方式见下文。 | ✓ | ✓ | |||
If-Unmodified-Since | 缓存协商校验字段,为上次请求收到的Last-Modified的值。处理方式与If-Modified-Since相反,见下文。 | ✓ | ✓ | |||
ETag | 请求资源的唯一标识字符串 | ✓ | ✓ | |||
If-Match | 缓存协商校验字段,请求资源的唯一标识字符串,为上次请求收到的ETag的值。处理方式见下文。 | ✓ | ✓ | |||
If-None-Match | 缓存协商校验字段,请求资源的唯一标识字符串,为上次请求收到的ETag的值。处理方式与If-Match相反,见下文。 | ✓ | ✓ |
注:乄表示半对,Last-Modified
之所以是半对,是因为有可能会触发启发式缓存,也会缓存文件。具体见下文。
缓存又分为强缓存和弱缓存(又称为协商缓存)。其中强缓存包括
Expires
和Cache-Control
,主要是在过期策略生效时应用的缓存。弱缓存包括Last-Modified
和ETag
,是在协商策略后应用的缓存。强弱缓存之间的主要区别在于获取资源时是否会发送请求。
2.1 Expires
如上所述,Expires
指定缓存的过期时间,为绝对时间,即某一时刻。参考本地时间进行比对,在指定时刻后过期。RFC 2616建议最大值不要超过1年。
Expire
头字段是响应头字段,格式如下:Expires: Sat Oct 20 2018 00:00:00 GMT+0800 (CST)
。
可以尝试以下步骤进行验证:
执行
node cache-Expires.js
,该脚本会给请求的资源设定Expires
,值为:”2018-10-20 00:00:00”。-
访问地址
http://localhost:1030/
,开启Network Tab,查看avatar.jpg图片,Expires值如下所示。 -
再次刷新会看到该资源已经被缓存,size栏显示为
(from memory cache)
。此时修改本地时间,将时间修改为“2018-10-15 00:00:00”,再刷新,会发现缓存仍然有效。 -
如果将本地时间修改为“2018-10-25 00:00:00”,再刷新,会发现图片不再使用缓存,而是重新获取了,因为本地时间超过了设定值。
Expires缓存过期,重新获取
2.2 Cache-Control
Cache-Control
用于指定资源的缓存机制,可以同时在请求头和响应头中设定,涉及上述三个策略中的两个策略:存储策略、过期策略。
Cache-Control
的语法如下:Cache-Control: cache-directive[,cache-directive]
。cache-directive
为缓存指令,大小写不敏感,共有12个与HTTP缓存标准相关,如下表所示。其中请求指令7种,响应指令9种。Cache-Control
可以设置多个缓存指令,以逗号,
分隔。
Key | 描述 | 存储策略 | 过期策略 | 请求字段 | 响应字段 |
---|---|---|---|---|---|
可缓存性相关 | --- | --- | --- | --- | --- |
public | 资源在客户端和代理服务器缓存 | ✓ | ✓ | ||
private | 资源仅在在客户端缓存,代理服务器不缓存 | ✓ | ✓ | ||
no-cache | 资源被缓存,但立即过期,下次访问时强制向服务器验证资源有效性。相当于max-age:0,must-revalidate
|
✓ | ✓ | ✓ | ✓ |
过期相关 | --- | --- | --- | --- | --- |
max-age=<seconds> | 在请求头中:指出客户端不接受有效时间大于指定时间的缓存。在响应头中:规定资源的最大新鲜时间,指定时间后过期,单位为秒。 | ✓ | ✓ | ✓ | ✓ |
s-maxage=<seconds> | 同上,但只对代理服务器生效,如果是private 缓存,会忽略该字段。会覆盖max-age 或Expires 头字段 |
✓ | ✓ | ✓ | |
max-stale=<seconds> | 指定时间内, 即使缓存过时, 资源依然有效 | ✓ | ✓ | ||
min-fresh=<seconds> | 缓存的资源至少要保持指定时间的新鲜期 | ✓ | ✓ | ||
验证与重载相关 | --- | --- | --- | --- | --- |
must-revalidate | 使用缓存资源之前,必须先验证状态,并且过期资源不应该再使用。 | ✓ | ✓ | ||
proxy-revalidate | 同上,但只对代理服务器生效,如果是private 缓存,会忽略该字段。 |
✓ | ✓ | ||
其他 | --- | --- | --- | --- | --- |
no-store | 请求和响应都不缓存 | ✓ | ✓ | ✓ | |
only-if-cached | 仅返回已经缓存的资源,不再向服务器获取新的内容。若无缓存则返回504 | ✓ | |||
no-transform | 强制要求代理服务器不要对资源进行转换, 禁止代理服务器对 Content-Encoding, Content-Range, Content-Type字段的修改(因此代理的gzip压缩将不被允许) | ✓ | ✓ |
2.3.1 cache-directive大小写不敏感
如上,cache-directive指令大小写不敏感,所以在设置Cache-Control时,指令可以不区分大小写。不过建议统一使用小写。验证如下:
- 执行
node cache-directive-case-insensitive.js
,会服务端会将max-age
写成大写,如下Cache-Control: MAX-AGE=86400
。 - 再次请求浏览器会发现缓存同样会生效。
2.3.2 在请求头中的max-age
max-age在请求头中的主要应用为max-age=0
表示不使用缓存。Chrome和Firefox浏览器下的刷新操作(Command+ R / F5)均是在请求头上添加了max-age=0�
指令,表示不使用强缓存,但允许协商缓存(在介绍了协商缓存的Last-Modified
和ETag
之后,可以自行验证下这一点)。
刷新时Cache-Control
为max-age=0
验证如下:
单独访问图片资源
http://localhost:1030/avatar.jpg
,开启Network-
刷新,可在响应头中看到上述内容。如下图所示。(Firefox下相同,不单独验证,主要最开始提到的主资源和派生资源在两个浏览器中表现形式的不同)。
此外,经验证,Chrome和Firefox均对max-age
>0的情况支持不好。 在Chrome下,通过
Modify Headers
插件(Chrome和Firefox下均有类似插件)给请求添加max-age=7200
。执行
node cache-max-age.js
,访问http://localhost:1030
,先强刷保证资源更新。-
打开NetWork,查看
avatar.jpg
,刷新,会发现,资源访问仍然走的是缓存。如果按照规范的定义应该是不生效。
2.3.3 max-age与Expires
Cache-Control中的max-age
指令用于指定缓存过期的相对时间。资源达到指定时间后过期。该功能与Expires类似。但其优先级高于Expires,如果同时设置max-age和Expires,max-age生效,忽略Expires。验证如下:
- 执行
node cache-max-age+Expires.js
,会同时设置Cache-Control: max-age=86400
/Expires: Mon Oct 20 2018 00:00:00 GMT+0800 (CST)
,如下所示。
- 刷新,然后再把本地时间改成当前时间延后2小时(不超过20号),会发现缓存生效。(以下两步不再附截图,与上述示例类似)。
- 如果将时间改为两天后(假设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-cache
和Cache-Control:no-cache
,并且不会带上协商策略中所涉及的信息(下面介绍的If-Modified-Since
/If-None-Match
)。这是不会使用任何缓存,重新获取资源。如下图所示。
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%作为缓存时间。验证如下:
- 执行
node cache-Last-Modified.js
,服务器会获取资源的最后修改时间,设置为Last-Modified
的值。访问localhost:1030
,查看avatar.jpg
,如下图所示:
- 刷新浏览器,会发现图片会从缓存获取。
- 通过启发式缓存的公司可以计算出缓存的时间,修改本地时间超过缓存时间后,再刷新,会发现缓存失效。
2.4.1 If-Modified-Since
返回的资源带有Last-Modified
标识时,再次请求该资源,浏览器会自动带上If-Modified-Since
,值为返回的Last-Modified
值。请求到达服务器后,服务器进行判断,如果从上次更新后没有再更新,则返回304。如果更新了则重新返回。验证如下:
- 执行
node cache-Last-Modified.js
,服务器会获取资源的最后修改时间,设置为Last-Modified
的值。如下图所示,并且注意看一下资源的大小。
-
刷新页面,再次查看NetWork。会发现请求头中带上了
If-Modified-Since
。如果服务器判断资源未改变,则返回304,此外由于服务器返回304,资源会从缓存获取,所以资源大小也减少了,如下所示。
-
修改
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)状态码的响应。主要有如下两种场景。
- 用于不安全的请求中从而是请求具备条件性(如POST或者其他不安全的方法),如请求更新wiki文档,文档未修改时才执行更新。
- 与
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
。当ETag
和Last-Modified
,ETag
优先级更高,但不会忽略Last-Modified
,需要服务端实现。验证如下,其中服务端判断优先级:
-
执行
node cache-ETag+Last-Modified.js
。服务端会在资源的响应头中,同时设置ETag
和Last-Modified
。如下图:
-
刷新浏览器,会发现
index.html
请求时304。查看node日志,会看到ETag生效
。如下:
三、缓存的优缺点
好了,通过长长的第二部分,我们简单介绍了一下HTTP Cache的基础知识。下面我再汇总一下各类缓存之间的优缺点吧。如下表所示:
缓存头部 | 优点 | 缺点 |
---|---|---|
Expires | 1. HTTP 1.0 产物,可以在HTTP 1.0和1.1中使用。2. 简单易用,通过绝对时间标识失效时间。 | 1. 时间为服务器返回的时间,如果本地时间与服务器时间不一致,则可能会出现问题。(如上述我们通过修改本地时间是缓存失效。)2. 存在版本问题,在资源过期之前如果对资源进行修改,客户端都是无法获知的。 |
Cache-Control | 1. HTTP 1.1的内容,以相对时间标识失效时间,解决了Expires服务器和客户端相对时间的问题。2. 支持的指令较多,可以根据需要进行相应的配置。 | 1. HTTP 1.1 才有的内容,不适用于HTTP 1.0 。2. 与Expires类似,存在版本问题,在资源过期之前如果对资源进行修改,客户端都是无法获知的。 |
Last-Modified | 1. 不存在版本问题,每次都会跟服务器进行校验,符合则304不返回资源,不符合则重新返回资源。 | 1. 以时刻作为标识,精确到秒,无法识别一秒内进行多次修改的情况。2. 只要资源被修改,无论内容是否发生实质性的变化,都会将该资源返回客户端。 |
ETag | 1. 不存在版本问题,每次都会跟服务器进行校验,符合则304不返回资源,不符合则重新返回资源。2. 可以更加精确的判断资源是否被修改。3. 可以识别一秒内多次修改的情况。 | 1. 计算ETag值会对性能造成一定消耗。2. 分布式服务器存储的情况下,需要保证计算ETag的算法一致。如果不一致,会导致资源在不同服务器上验证不通过。 |
四、最佳实践
从上面各类缓存的优缺点可以看出,每一种缓存都不是完美的。所以建议像下面这样做:
- 不要缓存HTML,避免缓存后用户无法及时获取到更新内容。
- 使用
Cache-Control
和ETag
来控制HTML中所使用的静态资源的缓存。一般是将Cache-Control
的max-age
设成一个比较大的值,然后用ETag
进行验证。 - 使用签名或者版本来区分静态资源。这样静态资源会生成不同的资源访问链接,不会产生修改之后无法感知的情况。
还有两个本文没有介绍的内容,但是不建议大家使用:
- 使用HTML的meta标签来指定缓存行为
- 使用查询字符串来避免缓存。因为缓存有一些已知的问题,使用查询字符串会导致有些代理服务器不缓存资源。
五、小试牛刀,看看你掌握了没有?
如果首次访问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命中强缓存,从缓存中换取。
总结
好了,文章到此结束,希望能对大家有帮助。