返回 登录
8

浏览器缓存机制剖析

阅读15458

作者:翟灿东,网名路易斯,平安健康前端工程师。有四年前端架构及开发经验。熟悉正则,坚持原著,深度思考,力求简单通俗叙事。博客地址: http://louiszhai.github.io
责编:陈秋歌,寻求报道或者投稿请发邮件至chenqg#csdn.net,或加微信:Rachel_qg。
了解更多前沿技术资讯,获取深度技术文章推荐,请关注CSDN研发频道微博

缓存一直是前端优化的主战场,利用好缓存就成功了一半。本篇从HTTP请求和响应的头域入手,让你对浏览器缓存有个整体的概念。最终你会发现强缓存,协商缓存 和 启发式缓存是如此的简单。

导读

浏览器对于请求资源,拥有一系列成熟的缓存策略。按照发生的时间顺序分别为存储策略过期策略协商策略,其中存储策略在收到响应后应用,过期策略协商策略在发送请求前应用。流程图如下所示。

Created with Raphaël 2.1.0发起请求是否已缓存?[过期策略]-缓存是否过期?[协商策略]-重新向服务器发起验证验证是否通过?304响应[存储策略]-根据响应头更新缓存载入资源向服务器请求资源[存储策略]-响应内容存入缓存yesnoyesnoyesno

废话不多说,我们先来看两张表格。

1.HTTP Header中与缓存有关的Key。

key 描述 存储策略 过期策略 协商策略
Cache-Control 指定缓存机制,覆盖其它设置 ✔️ ✔️
Pragma http1.0字段,指定缓存机制 ✔️
Expires http1.0字段,指定缓存的过期时间 ✔️
Last-Modified 资源最后一次的修改时间 ✔️
ETag 唯一标识请求资源的字符串 ✔️

2.缓存协商策略用于重新验证缓存资源是否有效,有关的Key如下。

key 描述
If-Modified-Since 缓存校验字段,值为资源最后一次的修改时间,即上次收到的Last-Modified值
If-Unmodified-Since 同上,处理方式与之相反
If-Match 缓存校验字段,值为唯一标识请求资源的字符串,即上次收到的ETag值
If-None-Match 同上,处理方式与之相反

下面我们来看下各个头域(key)的作用。

Cache-Control

浏览器缓存里,Cache-Control是金字塔顶尖的规则,它藐视一切其他设置,只要其他设置与其抵触,一律覆盖之。

不仅如此,它还是一个复合规则,包含多种值,横跨 存储策略过期策略 两种,同时在请求头和响应头都可设置。

语法为: “Cache-Control : cache-directive”

Cache-directive共有如下12种(其中请求中指令7种,响应中指令9种):

Cache-directive 描述 存储策略 过期策略 请求字段 响应字段
public 资源将被客户端和代理服务器缓存 ✔️ ✔️
private 资源仅被客户端缓存,代理服务器不缓存 ✔️ ✔️
no-store 请求和响应都不缓存 ✔️ ✔️ ✔️
no-cache 相当于max-age:0,must-revalidate即资源被缓存,但是缓存立刻过期,同时下次访问时强制验证资源有效性 ✔️ ✔️ ✔️ ✔️
max-age 缓存资源,但是在指定时间(单位为秒)后缓存过期 ✔️ ✔️ ✔️ ✔️
s-maxage 同上,依赖public设置,覆盖max-age,且只在代理服务器上有效。 ✔️ ✔️ ✔️
max-stale 指定时间内,即使缓存过时,资源依然有效 ✔️ ✔️
min-fresh 缓存的资源至少要保持指定时间的新鲜期 ✔️ ✔️
must-revalidation / proxy-revalidation 如果缓存失效,强制重新向服务器(或代理)发起验证(因为max-stale等字段可能改变缓存的失效时间) ✔️ ✔️
only-if-cached 仅仅返回已经缓存的资源,不访问网络,若无缓存则返回504 ✔️
no-transform 强制要求代理服务器不要对资源进行转换,禁止代理服务器对 Content-EncodingContent-RangeContent-Type字段的修改(因此代理的gzip压缩将不被允许) ✔️ ✔️

假设所请求资源于4月5日缓存,且在4月12日过期。

当max-age 与 max-stale 和 min-fresh 同时使用时,它们的设置相互之间独立生效,最为保守的缓存策略总是有效。这意味着,如果max-age=10 days,max-stale=2 days,min-fresh=3 days,那么:

  • 根据max-age的设置,覆盖原缓存周期, 缓存资源将在4月15日失效(5+10=15);
  • 根据max-stale的设置,缓存过期后两天依然有效,此时响应将返回110(Response is stale)状态码,缓存资源将在4月14日失效(12+2=14);
  • 根据min-fresh的设置,至少要留有3天的新鲜期,缓存资源将在4月9日失效(12-3=9);

由于客户端总是采用最保守的缓存策略,因此,4月9日后,对于该资源的请求将重新向服务器发起验证。

Pragma

HTTP1.0字段,通常设置为Pragma:no-cache,作用同Cache-Control:no-cache。当一个no-cache请求发送给一个不遵循HTTP/1.1的服务器时,客户端应该包含pragma指令。为此,勾选☑️ 上disable cache时,浏览器自动带上了pragma字段。如下:

Pragma:no-cache

Expires

Expires:Wed, 05 Apr 2017 00:55:35 GMT

即到期时间,以服务器时间为参考系,其优先级比 Cache-Control:max-age 低,两者同时出现在响应头时,Expires将被后者覆盖。如果ExpiresCache-Control: max-age,或 Cache-Control:s-maxage 都没有在响应头中出现,并且也没有其它缓存的设置,那么浏览器默认会采用一个启发式的算法,通常会取响应头的Date_value - Last-Modified_value值的10%作为缓存时间。

如下资源便采取了启发式缓存算法。

启发式缓存生效

其缓存时间为 `(Date_value - Last-Modified_value) * 10%,计算如下:

const Date_value = new Date('Thu, 06 Apr 2017 01:30:56 GMT').getTime();
const LastModified_value = new Date('Thu, 01 Dec 2016 06:23:23 GMT').getTime();
const cacheTime = (Date_value - LastModified_value) / 10;
const Expires_timestamp = Date_value + cacheTime;
const Expires_value = new Date(Expires_timestamp);
console.log('Expires:', Expires_value); // Expires: Tue Apr 18 2017 23:25:41 GMT+0800 (CST)

可见该资源将于2017年4月18日23点25分41秒过期,尝试以下两步进行验证:

1) 试着把本地时间修改为2017年4月18日23点25分40秒,迅速刷新页面,发现强缓存依然有效(依旧是200 OK (from disk cache))。

2) 然后又修改本地时间为2017年4月18日23点26分40秒(即往后拨1分钟),刷新页面,发现缓存已过期,此时浏览器重新向服务器发起了验证,且命中了304协商缓存,如下所示。

缓存过期,重新发起验证,命中304协商缓存

3) 将本地时间恢复正常(即 2017-04-06 09:54:19)。刷新页面,发现Date依然是4月18日,如下所示。

本地时间恢复正常,缓存依然有效

⚠️ Provisional headers are shown 和Date字段可以看出来,浏览器并未发出请求,缓存依然有效,只不过此时Status Code显示为200 OK。(甚至我还专门打开了charles,也没有发现该资源的任何请求,可见这个200 OK多少有些误导人的意味)

可见,启发式缓存算法采用的缓存时间可长可短,因此对于常规资源,建议明确设置缓存时间(如指定max-age 或 expires)。

ETag

ETag:"fcb82312d92970bdf0d18a4eca08ebc7efede4fe"

实体标签,服务器资源的唯一标识符,浏览器可以根据ETag值缓存数据,节省带宽。如果资源已经改变,etag可以帮助防止同步更新资源的相互覆盖。ETag 优先级比 Last-Modified 高。

If-Match

语法: If-Match: ETag_value 或者 If-Match: ETag_value, ETag_value, …

缓存校验字段,其值为上次收到的一个或多个etag 值。常用于判断条件是否满足,如下两种场景:

  • 对于 GET 或 HEAD 请求,结合 Range 头字段,它可以保证新范围的请求和前一个来自相同的源,如果不匹配,服务器将返回一个416(Range Not Satisfiable)状态码的响应。
  • 对于 PUT 或者其他不安全的请求,If-Match 可用于阻止错误的更新操作,如果不匹配,服务器将返回一个412(Precondition Failed)状态码的响应。

If-None-Match

语法: If-None-Match: ETag_value 或者 If-None-Match: ETag_value, ETag_value, …

缓存校验字段,结合ETag字段,常用于判断缓存资源是否有效,优先级比If-Modified-Since高。

  • 对于 GET 或 HEAD 请求,如果其etags列表均不匹配,服务器将返回200状态码的响应,反之,将返回304(Not Modified)状态码的响应。无论是200还是304响应,都至少返回 Cache-ControlContent-LocationDateETagExpires,and Vary 中之一的字段。
  • 对于其他更新服务器资源的请求,如果其etags列表匹配,服务器将执行更新,反之,将返回412(Precondition Failed)状态码的响应。

Last-Modified

语法: Last-Modified: 星期,日期 月份 年份 时:分:秒 GMT

Last-Modified: Tue, 04 Apr 2017 10:01:15 GMT

用于标记请求资源的最后一次修改时间,格式为GMT(格林尼治标准时间)。如可用 new Date().toGMTString()获取当前GMT时间。Last-Modified 是 ETag 的fallback机制,优先级比 ETag 低,且只能精确到秒,因此不太适合短时间内频繁改动的资源。不仅如此,服务器端的静态资源,通常需要编译打包,可能出现资源内容没有改变,而Last-Modified却改变的情况。

If-Modified-Since

语法同上,如:

If-Modified-Since: Tue, 04 Apr 2017 10:12:27 GMT

缓存校验字段,其值为上次响应头的Last-Modified值,若与请求资源当前的Last-Modified值相同,那么将返回304状态码的响应,反之,将返回200状态码响应。

If-Unmodified-Since

缓存校验字段,语法同上。表示资源未修改则正常执行更新,否则返回412(Precondition Failed)状态码的响应。常用于如下两种场景:

  • 不安全的请求,比如说使用post请求更新wiki文档,文档未修改时才执行更新。
  • 与 If-Range 字段同时使用时,可以用来保证新的片段请求来自一个未修改的文档。

强缓存

一旦资源命中强缓存,浏览器便不会向服务器发送请求,而是直接读取缓存。Chrome下的现象是 200 OK (from disk cache) 或者 200 OK (from memory cache)。如下:

200 OK (from disk cache)

200 OK (from memory cache)

对于常规请求,只要存在该资源的缓存,且Cache-Control:max-age 或者expires没有过期,那么就能命中强缓存。

协商缓存

缓存过期后,继续请求该资源,对于现代浏览器,拥有如下两种做法:

  • 根据上次响应中的ETag_value,自动往request header中添加If-None-Match字段。服务器收到请求后,拿If-None-Match字段的值与资源的ETag值进行比较,若相同,则命中协商缓存,返回304响应。
  • 根据上次响应中的Last-Modified_value,自动往request header中添加If-Modified-Since字段。服务器收到请求后,拿If-Modified-Since字段的值与资源的Last-Modified值进行比较,若相同,则命中协商缓存,返回304响应。

以上,ETag优先级比Last-Modified高,同时存在时,前者覆盖后者。下面通过实例来理解下强缓存和协商缓存。

如下忽略首次访问,第二次通过 If-Modified-Since 命中了304协商缓存。

304

协商缓存的响应结果,不仅验证了资源的有效性,同时还更新了浏览器缓存。主要更新内容如下:

Age:0
Cache-Control:max-age=600
Date: Wed, 05 Apr 2017 13:09:36 GMT
Expires:Wed, 05 Apr 2017 00:55:35 GMT

Age:0 表示命中了代理服务器的缓存,age值为0表示代理服务器刚刚刷新了一次缓存。

Cache-Control:max-age=600 覆盖 Expires 字段,表示从Date_value,即 Wed, 05 Apr 2017 13:09:36 GMT 起,10分钟之后缓存过期。因此10分钟之内访问,将会命中强缓存,如下所示:

200 from cache

当然,除了上述与缓存直接相关的字段外,http header中还包括如下间接相关的字段。

Age

出现此字段,表示命中代理服务器的缓存。它指的是代理服务器对于请求资源的已缓存时间,单位为秒。如下:

Age:2383321
Date:Wed, 08 Mar 2017 16:12:42 GMT

以上指的是,代理服务器在2017年3月8日16:12:42时向源服务器发起了对该资源的请求,目前已缓存了该资源2383321秒。

Date

指的是响应生成的时间。请求经过代理服务器时,返回的Date未必是最新的,通常这个时候,代理服务器将增加一个Age字段告知该资源已缓存了多久。

Vary

对于服务器而言,资源文件可能不止一个版本,比如说压缩和未压缩,针对不同的客户端,通常需要返回不同的资源版本。比如说老式的浏览器可能不支持解压缩,这个时候,就需要返回一个未压缩的版本; 对于新的浏览器,支持压缩,返回一个压缩的版本,有利于节省带宽,提升体验。那么怎么区分这个版本呢,这个时候就需要Vary了。

服务器通过指定Vary: Accept-Encoding,告知代理服务器,对于这个资源,需要缓存两个版本: 压缩和未压缩。这样老式浏览器和新的浏览器,通过代理,就分别拿到了未压缩和压缩版本的资源,避免了都拿同一个资源的尴尬。

Vary:Accept-Encoding,User-Agent

如上设置,代理服务器将针对是否压缩和浏览器类型两个维度去缓存资源。如此一来,同一个url,就能针对PC和Mobile返回不同的缓存内容。

怎么让浏览器不缓存静态资源

实际上,工作中很多场景都需要避免浏览器缓存,除了浏览器隐私模式,请求时想要禁用缓存,还可以设置请求头: Cache-Control: no-cache, no-store, must-revalidate

当然,还有一种常用做法: 即给请求的资源增加一个版本号,如下:

<link rel="stylesheet" type="text/css" href="../css/style.css?version=1.8.9"/>

这样做的好处就是你可以自由控制什么时候加载最新的资源。

不仅如此,HTML也可以禁用缓存,即在页面的\

<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"/>

上述虽能禁用缓存,但只有部分浏览器支持,而且由于代理不解析HTML文档,故代理服务器也不支持这种方式。

IE8的异常表现

实际上,上述缓存有关的规律,并非所有浏览器都完全遵循。比如说IE8。

资源缓存是否有效相关。

浏览器 前提 操作 表现 正常表现
IE8 资源缓存有效 新开一个窗口加载网页 重新发送请求(返回200) 展示缓存的页面
IE8 资源缓存失效 原浏览器窗口中单击 Enter 按钮 展示缓存的页面 重新发送请求(返回200)

Last-Modified / E-Tag 相关。

浏览器 前提 操作 表现 正常表现
IE8 资源内容没有修改 新开一个窗口加载网页 浏览器重新发送请求(返回200) 重新发送请求(返回304)
IE8 资源内容已修改 原浏览器窗口中单击 Enter 按钮 浏览器展示缓存的页面 重新发送请求(返回200)

本问就讨论这么多内容,大家有什么问题或好的想法欢迎在下方参与留言和评论

参考文章

欢迎加入“CSDN前端开发者”群,与更多专家、技术同行进行热点、难点技术交流。请扫描以下二维码加群主微信,申请入群,务必注明「姓名+公司+职位」
图片描述

评论