• home > webfront > SGML > web >

    浏览器http缓存机制剖析:存储策略与过期策略的机理分析

    Author:zhoulujun Date:

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

    浏览器为了节约网络的资源与加速浏览,会在用户磁盘上对最近请求过的文档进行存储。当访问者再次请求这个页面时,浏览器就可以从本地磁盘显示文档,这样就可以加速页面的阅览。

    浏览器存在默认缓存行为,但是不同浏览器的默认缓存策略是不一样,所以我们需要设置浏览器缓存。明确的是:浏览器缓存设置是由服务器端生成的。

    最初我只知道Expires、Cache-Control、Last-Modified、ETag都是在http response的返回header中用来控制浏览器客户端缓存行为的。但是浏览器这些字段背后的原理只知道个大概,所以今天特别来梳理下浏览器缓存机制

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

    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指定缓存机制,覆盖其它设置✔️✔️
    Pragmahttp1.0字段,指定缓存机制✔️

    Expireshttp1.0字段,指定缓存的过期时间
    ✔️
    Last-Modified资源最后一次的修改时间

    ✔️
    ETag唯一标识请求资源的字符串

    ✔️

    缓存协商策略用于重新验证缓存资源是否有效, 有关的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指定时间内, 即使缓存过时, 资源依然有效
    ✔️✔️
    stale-while-revalidate允许你使用缓存的内容尽快响应请求(如果可用),如果未缓存,则回退到网络请求。 然后,网络请求用于更新缓存。✔️✔️
    ✔️
    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才是真正的不进行缓存

    stale-while-revalidate VS max-age

    stale-while-revalidate比较适用的场景是,我们查询的信息需要被刷新,但一定程度的陈旧是可以接受的(stale-while-revalidate最初是为缓存服务器设计的)。

    stale-while-revalidate对非覆盖式发布的资源没有用处

    资源的 URL 是不能变的,可以考虑 stale-while-revalidate,比如网站首页。其次是类似PWA serviceWork 主动资源刷新方案。

    了加载速度牺牲了资源的新鲜性(通常就牺牲一次),但有些场景下,可能一次也无法接受,比如就说淘宝首页,给它指定个一天的 stale-while-revalidate,那假如用户在双十一前一天访问过首页,页面会被缓存下来,然后第二天也就是双十一早上再打开,可能会发现页面和昨天一模一样,并没有双十一氛围的样式,虽然只有一次,但可能也无法接受。

    stale-while-revalidate该不该用,该用多大的数字,需要自己视情况而定。stale-while-revalidate大、max-age小应该是主流的选择。例如:

    Cache-Control: max-age=60, stale-while-revalidate=3600, must-revalidate

    缓存在 60 秒内是新鲜的,从 60 秒到 3660 秒的这一个小时内,虽然缓存是过期了,但仍可以直接使用这个过期缓存,同时进行异步 revalidate,在 3660 秒之后,就是完全过期了,需要进行传统的同步 revalidate。

    public VS private

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

    • private资源仅被客户端缓存

    • public:客服端与资源所过服务器都可以缓存——处处留痕。

    其实这个属性只是君子之约而已,为了安全,最好还是上https 加密传输内容,这样即使被缓存了也是密文而已。

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

    v2-95444200875d9cdc6783deb48e72da6c_hd.jpg


    Expires

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

    即到期时间, 以服务器时间为参考系,在指定的日期到达之前再次访问则认为缓存有效,其优先级比 Cache-Control:max-age 低, 两者同时出现在响应头时, Expires将被后者覆盖(Expires在HTTP/1.0中已经定义,Cache-Control:max-age=xxx在HTTP/1.1中才有定义,为了向下兼容,仅使用max-age不够)

    如果Expires Cache-Control: max-ageCache-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都是用于控制缓存的生命周期。

    1. Expires指定的是过期的具体时间,例如Sun, 21 Mar 2027 08:52:14 GMT,而max-age指定的是生命时长秒数315360000

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

    3.  Cache-Control 的选择更多,设置更细致,如果同时设置的话,其优先级高于 Expires。

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

    判断文件变动

    常用的方式为Etag和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

    1. 当第一次请求某一个文件的时候,就会传递回来一个Last-Modified 字段,其内容是这个文件的修改时间

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

    Last-Modified

    语法: Last-Modified: 星期,日期 月份 年份 时:分:秒 GMT。一般会选文件的 mtime,表示文件内容的修改时间(nginx 也是这样处理的,源码见: ngx_http_static_module.c

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

    用于标记请求资源的最后一次修改时间, 格式为GMT(格林尼治标准时间)。如可用 new Date().toGMTString()获取当前GMT时间。

    Last-Modified是 ETag 的fallback机制, 优先级比 ETag 低, 且只能精确到秒,因此不太适合短时间内频繁改动的资源。不仅如此,服务器端的静态资源, 通常需要编译打包, 可能出现资源内容没有改变,  而Last-Modified却改变的情况。还有可能Last-Modified相同,文件却不同的情况。因为Last-Modified无法确保文件的唯一性。

    浏览器会向服务器传送 If-Modified-Since 报头(Http Request Header),询问该时间之后文件是否有被修改过

    If-Modified-Since

    语法同上,如:

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

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

    ETag

    ETag:"fcb82312d92970bdf0d18a4eca08ebc7efede4fe"1

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

    客户端的查询更新格式是这样的:

    If-None-Match: "5d8c72a5edda8d6a:3239″

    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-Control, Content-Location, Date, ETag, Expires, and Vary 中之一的字段.

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

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

    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-Unmodified-Since

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

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

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

    Etag VS. Last-Modified

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

    我们把这两者都成为验证器(Validators),不同的是,

    • Etag属于强验证(Strong Validation),因为它期望的是资源字节级别的一致

      • Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。比如可以直接使用文件的MD5值,当然也可以自定义(nginx-static-etags模块源码:https://github.com/mikewest/nginx-static-etags.git),比如 七牛。普通用户直接修改 src/http/modules/ngx_http_core_modules.c 文件 Line 1600行出etag赋值,追加自定义算法——nginx 中的 etag 由 last_modified 与 content_length 组成,而 last_modified 又由 mtime 组成。1s 内更改文件,并且保持文件大小不变。那么,nginx默认etag可能不变

        注意:ETag摘要算法生成 (MD5, SHA128, SHA256) 需要慎重考虑,因为他们是 CPU 密集型运算

      • etag 的生成算法有可能会用文件所在服务器的一些信息包含计算,所以导致同一个文件算在不同服务器下算出来的值不一样,也就是多次请求同一个文件的时候命中了不同服务器导致无法触发协商缓存。

        注意:分布式服务注意ETag算法的一致性

    • Last-Modified属于弱验证(Weak Validation),只要资源的主要内容一致即可,允许例如页底的广告,页脚不同

      • 如果某些文件会被定期生成,当有时内容并没有任何变化,但Last-Modified却改变了,导致文件没法使用缓存

      • Last-Modified标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的新鲜度

      • 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形

    根据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状态。但话说回来,是否返回还是通过服务端编写的实际代码决定的。所以仍然有操纵的空间。

    Last-Modified/ETag与Cache-Control/Expires

    • 配置Last-Modified/ETag的情况下,浏览器再次访问统一URI的资源,还是会发送请求到服务器询问文件是否已经修改,如果没有,服务器会只发送一个304回给浏览器,告诉浏览器直接从自己本地的缓存取数据;如果修改过那就整个数据重新发给浏览器;

    • Cache-Control/Expires则不同,如果检测到本地的缓存还是有效的时间范围内,浏览器直接使用本地副本,不会发送任何请求。两者一起使用时,Cache-Control/Expires的优先级要高于Last-Modified/ETag。即当本地副本根据Cache-Control/Expires发现还在有效期内时,则不会再次发送请求去服务器询问修改时间(Last-Modified)或实体标识(Etag)了。

    一般情况下,使用Cache-Control/Expires会配合Last-Modified/ETag一起使用,因为即使服务器设置缓存时间, 当用户点击“刷新”按钮时,浏览器会忽略缓存继续向服务器发送请求,这时Last-Modified/ETag将能够很好利用304,从而减少响应开销。

    用户操作行为与缓存

    用户在使用浏览器的时候,会有各种操作,比如输入地址后回车,按F5刷新等,这些行为会对缓存有什么影响呢?

    9.png

    强缓存

    一旦资源命中强缓存, 浏览器便不会向服务器发送请求, 而是直接读取缓存. 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返回不同的缓存内容。这个问题出现在 PC 端和移动端是两套代码,却共用一个资源路径(如域名或者url)。但最好不要出现这种情况,PC 端和移动端如果是两套代码,建议用两个域名,理由如下

    • nginx 判断是否移动端容易出错

    • 对缓存不友好


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

    实际上, 工作中很多场景都需要避免浏览器缓存, 除了浏览器隐私模式, 请求时想要禁用缓存, 还可以设置请求头: 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)

    服务器如何配置Etag和Expires

    Apache、Lighttpd和Nginx中针配置Etag和Expires,有效缓存纯静态如css/js/pic/页面/流媒体等文件。

    Expires

    对于不常修改的文件,可以通过 expires 指令来控制其在浏览器的缓存,以减少不必要的请求。

    expires 指令可以控制 HTTP 应答中的" Expires "和" Cache-Control "的头标(起到控制页面缓存的作用)。

    Apache Expires

    使用Apache的mod_expires 模块来设置,这包括控制应答时的Expires头内容和Cache-Control头的max-age指令

    ExpiresActive On
    ExpiresByType image/gif "access plus 1 month"
    ExpiresByType image/jpg "access plus 1 month"
    ExpiresByType image/jpeg "access plus 1 month"
    ExpiresByType image/x-icon "access plus 1 month"
    ExpiresByType image/bmp "access plus 1 month"
    ExpiresByType image/png "access plus 1 month"
    ExpiresByType text/html "access plus 30 minutes"
    ExpiresByType text/css  "access plus 30 minutes"
    ExpiresByType text/txt  "access plus 30 minutes"
    ExpiresByType text/js   "access plus 30 minutes"
    ExpiresByType application/x-javascript   "access plus 30 minutes"
    ExpiresByType application/x-shockwave-flash     "access plus 30 minutes"

    当设置了expires后,会自动输出Cache-Control 的max-age 信息
    具体关于 Expires 详细内容可以查看Apache官方文档
    在这个时间段里,该文件的请求都将直接通过缓存服务器获取
    当然如果需要忽略浏览器的刷新请求(F5),缓存服务器squid还需要使用 refresh_pattern 选项来忽略该请求

    Squid Expires

    refresh_pattern -i \.gif$ 1440 100% 28800 ignore-reload
    refresh_pattern -i \.jpg$ 1440 100% 28800 ignore-reload
    refresh_pattern -i \.jpeg$ 1440 100% 28800 ignore-reload
    refresh_pattern -i \.png$ 1440 100% 28800 ignore-reload
    refresh_pattern -i \.bmp$ 1440 100% 28800 ignore-reload
    refresh_pattern -i \.htm$ 60 100% 100 ignore-reload
    refresh_pattern -i \.html$ 1440 50% 28800 ignore-reload
    refresh_pattern -i \.xml$ 1440 50% 28800 ignore-reload
    refresh_pattern -i \.txt$ 1440 50% 28800 ignore-reload
    refresh_pattern -i \.css$ 1440 50% 28800 reload-into-ims
    refresh_pattern -i \.js$ 60 50% 100 reload-into-ims
    refresh_pattern . 10 50% 60

    有关Squid中Expires的说明,请参考Squid官方中refresh_pattern介绍。

    Lighttpd Expires

    和Apache一样Lighttpd设置expire也要先查看是否支持了mod_expire模块,

    下面的设置是让URI中所有images目录下的文件1小时后过期;

    expire.url = ( "/images/" => "access 1 hours" )

    下面是让作用于images目录及其子目录的文件;

    $HTTP["url"] =~ "^/images/" {
        expire.url = ( "" => "access 1 hours" )
    }

    也可以指定文件的类型;

    $HTTP["url"] =~ "\.(jpg|gif|png|css|js)$" {
        expire.url = ( "" => "access 1 hours" )
    }

    具体参考Lighttpd官方Expires解释
    Nginx中Expires

    location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
        expires 30d;
    }
    location ~ .*\.(js|css)?$ {
        expires 1h;
    }

    其他请参考Nginx中Expires

    Etag

    Apache中Etag设置

    在Apache中设置Etag的支持比较简单,只用在含有静态文件的目录中建立一个文件.htaccess, 里面加入:

    FileETag MTime Size

    这样就行了,详细的可以参考Apache的FileEtag文档页

    Lighttpd Etag

    在Lighttpd中设置Etag支持:

    1. etag.use-inode: 是否使用inode作为Etag

    2. etag.use-mtime: 是否使用文件修改时间作为Etag

    3. etag.use-size: 是否使用文件大小作为Etag

    4. static-file.etags: 是否启用Etag的功能

    第四个参数肯定是要enable的, 前面三个就看实际的需要来选吧,推荐使用修改时间

    Nginx Etag

    对于版本为1.7.3及更高的版本,打开Nginx的配置文件nginx.conf(默认位置Nginx安装目录/conf/),确保其中没有出现etagoff;

    http{
        etag: on;
        gzip: on;
    }

    对于1.7.3以下的版本,Nginx中默认没有添加对Etag标识Igor Sysoev的观点"在对静态文件处理上看不出如何Etag好于Last-Modified标识。"

    Note:Yes, it's addition,and it's easy to add, however, I do not see howETag is better than Last-Modified for static files. -Igor Sysoev
    A nice short description is here:
    http://www.mnot.net/cache_docs/#WORK
    It looks to me that it makes some caches out there to cache theresponse from the origin server more reliable as in rfc2616(ftp://ftp.rfc-editor.org/in-notes/rfc2616.txt) is written.
    3.11 Entity Tags 13.3.2 Entity Tag Cache Validators 14.19 ETag
    当然也有第三方nginx-static-etags 模块了,请参考
    http://mikewest.org/2008/11/generating-etags-for-static-content-using-nginx

    对于非实时交互动态页面中Epires和Etag处理

    对数据更新并不频繁、如tag分类归档等等,可以考虑对其cache。简单点就是在非实时交互的动态程序中输出expires和etag标识,让其缓存。但需要注意关闭session,防止http response时http header包含session id标识;

    Expires

    如expires.php

    <?php
    header('Cache-Control: max-age=86400,must-revalidate');
    header('Last-Modified: ' .gmdate('D, d M Y H:i:s') . ' GMT' );
    header("Expires: " .gmdate ('D, d M Y H:i:s', time() + '86400′ ). ' GMT');
    ?>

    以上信息表示该文件自请求后24小时后过期。
    其他需要处理的动态页面直接调用即可。

    Etag

    根据Http返回状态来处理。当返回304直接从缓存中读取

    如etag.php

    <?php
    cache();
    echo date("Y-m-d H:i:s");
    function cache(){
        $etag = "http://www.jb51.net";
        if ($_SERVER['HTTP_IF_NONE_MATCH'] == $etag){
            header('Etag:'.$etag,true,304);
            exit;
        }else {
            header('Etag:'.$etag);
        }
    }
    ?>
    1. 几个不同或者需要补充的地方: 
      “当然,Etag 对多数站点性能的影响并不是很大。”应该说 Etag 在正确使用的情况下,会让大量的请求以 304 头方式响应,可以相当的节省服务器资源和带宽。之前一些地方写的不要使用 Etag,是基于有些 webserver 的 Etag 的计算方法中包含了 inode,这在多台web服务器的情况不可采用的,而改变这个计算方法就可以了。 

    2. 对于尽早刷新这点,PHP 几乎是做不到的。即使你执行了 flush 以及类似的函数,也要等到请求完全执行之后,才会输出给浏览器端。 

    3. AJAX 使用 GET 和 POST 各有好处GET 方式可以更快响应,但是可能会有被浏览器缓存的问题,一般都需要加个随机数来避免,POS方式则不会。所以最好是根据自己的情况分别使用 GET 和 POST 方法。


    之前想写这方面的内容,弄了些,然后发现 alloyteam的《【Web 缓存机制系列】2 – Web 浏览器的缓存机制》写的好,就copy了一把,然后 再copy了一把 louis的《浏览器缓存机制剖析》……反正就是这么断断续续地修改与增补。整站风格都是如此,如果不妥之处,请留言告知,及时更新本文。尽量保持最详细与简洁。


    参考文章





    转载本站文章《浏览器http缓存机制剖析:存储策略与过期策略的机理分析》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/SGML/web/2012_0215_8074.html