zoukankan      html  css  js  c++  java
  • 浏览器缓存机制剖析

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

    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 .

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

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

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

    不仅如此, HTML也可以禁用缓存, 即在页面的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的《浏览器缓存机制剖析》

    此文如有不妥之处,请告知,谢谢!

    参考文章

  • 相关阅读:
    基本技能训练之线程
    关于UEditor的使用配置(图片上传配置)
    PAT 乙级练习题1002. 写出这个数 (20)
    codeforces 682C Alyona and the Tree DFS
    codeforces 681D Gifts by the List dfs+构造
    codeforces 678E Another Sith Tournament 概率dp
    codeforces 680E Bear and Square Grid 巧妙暴力
    codeforces 678D Iterated Linear Function 矩阵快速幂
    codeforces 679A Bear and Prime 100 交互
    XTUOJ 1248 TC or CF 搜索
  • 原文地址:https://www.cnblogs.com/zhoulujun/p/9071277.html
Copyright © 2011-2022 走看看