Http 缓存剖析

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

导读

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

 

发起请求是否已缓存?[过时策略]-缓存是否过时?[协商策略]-从新向服务器发起验证验证是否经过?304响应[存储策略]-根据响应头更新缓存载入资源向服务器请求资源[存储策略]-响应内容存入缓存是不是否是否

废话很少说, 咱们先来看两张表格.html

1.http header中与缓存有关的key.前端

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

2.缓存协商策略用于从新验证缓存资源是否有效, 有关的key以下.java

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

下面咱们来看下各个头域(key)的做用.git

Cache-Control

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

不只如此, 它仍是一个复合规则, 包含多种值, 横跨 存储策略, 过时策略 两种, 同时在请求头和响应头均可设置.算法

语法为: “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字段. 以下:

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协商缓存, 以下所示.

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). 以下:

对于常规请求, 只要存在该资源的缓存, 且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 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分钟以内访问, 将会命中强缓存, 以下所示:

固然, 除了上述与缓存直接相关的字段外, 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也能够禁用缓存, 即在页面的\<head\>节点中加入\<meta\>标签, 代码以下:

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

版权声明:转载需注明做者和出处。

本文做者:louis

本文连接:http://louiszhai.github.io/2017/04/07/http-cache/

参考文章

相关文章
相关标签/搜索