浏览器缓存机制剖析


浏览器对于请求资源, 流程如图所示:

1.png

可以看到浏览器的缓存机制分为两个部分:

1、当前缓存是否过期?

2、服务器中的文件是否有改动?

第一步:判断当前缓存是否过期

这是判断是否启用缓存的第一步。如果浏览器通过某些条件(条件之后再说)判断出来,ok现在这个缓存没有过期可以用,那么连请求都不会发的,直接是启用之前浏览器缓存下来的那份文件,此时状态码为200

第二步:判断服务器中的文件是否有改动

1、缓存过期,文件有改动,那么下载新文件,此时状态码为200

2、缓存过期,文件无改动,那么服务器只会给你返回一个头信息(304),浏览器读取304后,就会去读取过期缓存文件。

如何判断缓存的过期以及文件的变动?

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

Screen-Shot-2018-05-18-at-20.34.01.png

判断缓存过期,主要还是靠HTTP头,废话不多说, 我们先来看两张表格

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日后, 对于该资源的请求将重新向服务器发起验证.

技术细节:must-revalidate,no-cache,max-age=0,no-store,             

  • must-revalidate:   如果你配置了max-age信息,当缓存资源仍然新鲜(小于max-age)时使用缓存,否则需要对资源进行验证。所以must-revalidate可以和max-age组合使用Cache-Control: must-revalidate, max-age=60

  • no-cache: 虽然字面意义是“不要缓存”。但它实际上的机制是,仍然对资源使用缓存,但每一次在使用缓存之前必须(MUST)向服务器对缓存资源进行验证。

  • max-age=0:告知浏览器,资源已经过期了,你应该(SHOULD)对资源进行重新验证了;在重新获取资源之前,先检验ETag/Last-Modified。而no-cache则是告诉浏览器在每一次使用缓存之前,你必须(MUST)对资源进行重新验证。

    区别在于:SHOULD是非强制性的,而MUST是强制性的。在no-cache的情况下,浏览器在向服务器验证成功之前绝不会使用过期的缓存资源,而max-age=0则不一定了。

  • no-store:  不使用任何缓存。有趣的事情是,虽然no-cache意为对缓存进行验证,但是因为大家广泛的错误的把它当作no-store来使用,所以有的浏览器也就附和了这种设计。这是一个典型的劣币驱逐良币

不管是max-age=0还是no-cache,都会返回304(资源无修改的情况下),no-store才是真正的不进行缓存

public VS. private

要知道从服务器到浏览器之间并非只有浏览器能够对资源进行缓存,服务器的返回可能会经过一些中间(intermediate)服务器甚至甚至专业的中间缓存服务器,还有CDN。而有些请求返回是用户级别、是私人的,所以你可能不希望这些中间服务器缓存返回。此时你需要将Cache-Control设置为private以避免暴露。

所以综上,关于如何设计缓存机制,还是要依据你的需求而定,可以通过下面的这棵决策树决定:

v2-95444200875d9cdc6783deb48e72da6c_hd.jpg

Expires

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

即到期时间, 以服务器时间为参考系, 其优先级比 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)123456

可见该资源将于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协商缓存, 如下所示.

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).

Expires VS. max-age

Expires和max-age都是用于控制缓存的生命周期。不同的是Expires指定的是过期的具体时间,例如Sun, 21 Mar 2027 08:52:14            GMT,而max-age指定的是生命时长秒数315360000。

区别在于Expires是 HTTP/1.0 的中的标准,而max-age是属于Cache-Control的内容,是 HTTP/1.1 中的定义的。但为了想向前兼容,这两个属性仍然要同时存在。

但有一种更倾向于使用max-age的观点认为Expires过于复杂了。例如上面的例子Sun, 21 Mar 2027 08:52:14            GMT,如果你在表示小时的数字缺少了一个0,则很有可能出现出错;如果日期没有转换到用户的正确时区,则有可能出错。这里出错的意思可能包括但不限于缓存失效、缓存生命周期出错等。

判断文件变动

常用的方式为Etag和Last-Modified,思路上差不多,这里作者只介绍Last-Modified的用法。

Last-Modified方式需要用到两个字段:Last-Modified & if-modified-since。

先来看下这两个字段的形式:

Last-Modified : Fri , 12 May 2006 18:53:33 GMT

If-Modified-Since : Fri , 12 May 2006 18:53:33 GMT

可以看出其实形式是一样的,就是一个标准时间。那么怎么用呢?来看下图:

8.png

当第一次请求某一个文件的时候,就会传递回来一个Last-Modified 字段,其内容是这个文件的修改时间。当这个文件缓存过期,浏览器又向服务器请求这个文件的时候,会自动带一个请求头字段If-Modified-Since,其值是上一次传递过来的Last-Modified的值,拿这个值去和服务器中现在这个文件的最后修改时间做对比,如果相等,那么就不会重新拉取这个文件了,返回304让浏览器读过期缓存。如果不相等就重新拉取。

Last-Modified

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

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

用于标记请求资源的最后一次修改时间, 格式为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 GMT1

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

 

ETag

ETag:"fcb82312d92970bdf0d18a4eca08ebc7efede4fe"1

实体标签, 服务器资源的唯一标识符, 浏览器可以根据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-LocationDate,                    ETagExpires, and Vary 中之一的字段.

  • 对于其他更新服务器资源的请求, 如果其etags列表匹配, 服务器将执行更新, 反之, 将返回412(Precondition Failed)状态码的响应。

If-Unmodified-Since

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

  • 不安全的请求, 比如说使用post请求更新wiki文档, 文档未修改时才执行更新.

  • 与 If-Range 字段同时使用时, 可以用来保证新的片段请求来自一个未修改的文档.

ETag&(If-Match&If-None-Match)关系如同Last-Modified&if-modified-since。

 

Etag VS. Last-Modified

Etag和Last-Modified都可以用于对资源进行验证,而Last-Modified顾名思义,表示资源最后的更新时间。

我们把这两者都成为验证器(Validators),不同的是,Etag属于强验证(Strong Validation),因为它期望的是资源字节级别的一致;而Last-Modified属于弱验证(Weak Validation),只要资源的主要内容一致即可,允许例如页底的广告,页脚不同。

根据RFC 2616标准中的13.3.4小节,一个使用HTTP 1.1标准的服务端应该(SHOULD)同时发送Etag和Last-Modified字段。同时一个支持HTTP 1.1的客户端,比如浏览器,如果服务端有提供Etag的话,必须(MUST)首先对Etag进行Conditional Request(If-None-Match头信息);如果两者都有提供,那么应该(SHOULD)同时对两者进行Conditional Request(If-Modified-Since头信息)。如果服务端对两者的验证结果不一致,例如通过一个条件判断资源发生了更改,而另一个判定资源没有发生更改,则不允许返回304状态。但话说回来,是否返回还是通过服务端编写的实际代码决定的。所以仍然有操纵的空间。

强缓存

一旦资源命中强缓存, 浏览器便不会向服务器发送请求, 而是直接读取缓存. Chrome下的现象是 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协商缓存.

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

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

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

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

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

Pragma

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

Age

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

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

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

Date

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

Vary

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

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

Vary:Accept-Encoding,User-Agent1

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

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

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

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

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

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

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

IE8的异常表现

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

资源缓存是否有效相关.

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

Last-Modified / E-Tag 相关.

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

本文改编自louis的《浏览器缓存机制剖析》

文章来源:浏览器缓存机制剖析 - Standard Generalized Markup langu - 周陆军的个人网站 此文如有不妥之处,请告知,谢谢!

参考文章

  • Cache Policy Interaction—Maximum Age and Maximum Staleness

  • HTTP/1.1: Header                        Field Definitions

  • http - What’s the difference between Cache-Control: max-age=0 and no-cache? -                        Stack Overflow

  • App 缓存方案:Http 缓存 · baitouwei

  • Cache-Control                        - HTTP | MDN

  • 彻底弄懂 Http 缓存机制 基于缓存策略三要素分解法

  • 浏览器缓存机制剖析

  • 设计一个无懈可击的浏览器缓存方案:关于思路,细节,ServiceWorker,以及HTTP/2

  • no-cache,max-age=0,nostore区别及304原理

  • 【Web缓存机制系列】2 – Web浏览器的缓存机制