前端性能优化面面谈[2]-缓存

浏览器特性:

关于缓存,我们要知道一些常用的chrome浏览器行为,也就是说我们做一些操作的时候,chrome会对缓存做哪些处理。基于mac上面chrome浏览器的测试,我得出如下的结论:

  1. 在直接输入网址访问的时候,发现浏览器针对html中引用的其他资源(例如css、字体等)会按照我们常见的流程去走: 先看资源有没有强缓存,再看强缓存有没有过期,过期了才给服务器发请求,发请求的时候看看资源有没有etag和last-modify, 有则在HTTP头中带上 if-none-match: etag 值和 if-modify-since: modify 值。如果本地根本没有缓存,则浏览器会发送一个完全全新的http请求给服务器。

    而对于index.html,你如果直接输入网址访问 chrome浏览器为了快速打开会直接使用一个本地的缓存,无论index.html有没有被缓存(实际上可能是浏览器偷偷缓存了一个index.html);测试方法为先打开一个新标签,然后打开开发人员工具,然后输入网址,再回车。(从表现看,这里有可能会造成服务器更新了html不生效,不知chrome是怎么想的)

  2. 如果不是直接访问网址。 而是点浏览器刷新,或ctrl+r, 则访问index.html时会给服务器发请求(因为index.html一般不缓存,上次响应时一般没有设置过缓存字段,所以刷新请求时只会带上 Cache-Control: max-age=0)

    而对于index.html中引用的其他各种资源(如css、font等),由于大多数情况下都是有缓存的,所以你刷新后 基本上都是 from dist cachefrom memory cache.

  3. 如果把html里面引用的js路径拷出来在浏览器中访问。点击刷新按钮。则会向服务器发起请求,服务器会进行缓存协商 一般js这种资源会返回304.

  4. 如果是点ctrl+f5强制刷新,这叫 非缓存刷新 (或者右键点击chrome的刷新按钮选择硬性重新加载, 或者开发人员工具里diable cache),此时,浏览器则任何etag啥的都不检查,就认为本地没有缓存,然后直接发送一个最原始的http报文到服务器,且请求里面的cache-control是no-cache。 (由于HTTP请求中没有last-modify-since,也没有if-none-match这些头,所以服务器肯定不会返回304,而是重发一遍资源)。
    而且此时浏览器会带上一个特殊的http头: Cache-Control: no-cache ,这个头是强调客户端不希望请求缓存的资源的意思,相当于更显式地告诉服务器我要重新拉一遍资源。

从上面看出,index.html一般是不缓存的;css、js等资源是缓存的。 index.html的不缓存体现在,无论怎样请求,他的响应里总是包含:

1
2
Cache-Control: private 或 no-cache
Expires: Tue, 15 May 2018 03:29:28 GMT

也就是说 Cache-Control: 不缓存, 过期时间: 当前时间

而css、js资源,一般会有如下4个缓存字段:

1
2
3
4
5
ETag: W/"59f2e1a6-19baa"
Expires: Wed, 23 May 2018 01:12:28 GMT

Cache-Control: max-age=2592000
Last-Modified: Fri, 27 Oct 2017 07:35:02 GMT

http头里缓存属性

我们谈到缓存的时候,会发现有好几种: expire, etag, cacheControl, last-modify。

当请求一个资源时,浏览器会先判断是否 强缓存, 强缓存且有效则直接读取本地资源;如果缓存过期了,再进行缓存协商(即发起请求,等待304或者200)。 本地没有缓存的情况我们就不研究了,他自然是一次全新的请求。

所以缓存场景 这里有2个步骤,第一: 是服务器要告诉浏览器 强缓存的时长;第二: 一旦缓存过期则浏览器要跟服务器进行 协商缓存

  • 第一个步骤: Expire和Cache_Control
    Expires是http1.0提出的一个表示资源过期时间的header,它描述的是一个绝对时间,由服务器返回,用GMT格式的字符串表示(注: GMT表示格林威治标准时间,等同于UTC世界协调时间)。
    Cache-Control描述的是一个相对时间,在进行缓存命中的时候,都是利用客户端时间进行判断,所以相比较Expires,Cache-Control的缓存管理更有效,安全一些。
    当两个同时出现时,以Cache-control为准,为什么呢? 毕竟http1.1提出的东西肯定要比http1.0的expire更牛逼一点吧

    cache-control的字段说明:

    1
    2
    3
    4
    5
    no-cache:不使用强缓存,使用协商缓存
    no-store:所有内容都不会缓存
    private:默认值,只能被客户端用户缓存,不能被cdn缓存
    public:可以被所有的客户端和cdn缓存
    max-age:缓存内容将会在指点多少秒后失效

    当然,在第一个步骤中,服务器除了告诉浏览器 强缓存的策略,其实也为将来 协商缓存 埋下了伏笔。即: 顺便给了浏览器 EtagLast-Modified 俩字段。

  • 第二个步骤: 缓存协商. If-Modified-Since 和 If-None-Math

    If-Modified-Since:当资源过期时(强缓存失效),发现资源具有Last-Modified声明,则再次向web服务器请求时带上头 If-Modified-Since,表示请求时间。web服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应整片资源内容(写在响应消息包体内),HTTP 200;若最后修改时间较旧,说明资源无新修改,则响应HTTP 304 (无需包体,节省浏览),告知浏览器继续使用所保存的cache。

    web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。
    If-None-Match:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Etage声明,则再次向web服务器请求时带上头If-None-Match (Etag的值)。web服务器收到请求后发现有头If-None-Match 则与被请求资源的相应校验串进行比对,决定返回200或304。

    Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。

另外,http头里一般还有个Date字段,这个字段是做什么呢?

答: Date头域表示消息发送的时间,时间的描述格式由rfc822定义。例如,Date:Mon,31Dec200104:25:57GMT。Date描述的时间表示世界标准时,换算成本地时间,需要知道用户所在的时区. 可以说,基本上这个字段对浏览器和客户端程序来说: 没啥用。

关于缓存协商,这里有个非常清晰的协商策略图:

协商缓存流程

浏览器何时会发送缓存检测请求?

第一节已经讨论过了chrome浏览器的行为。所以能够发起探测请求的情况有:

  1. 输入网址访问index.html后,html里引用的其他资源过期了
    `由于引用的css,png,font等资源过期了,所以这些资源必然会发起向服务器的请求

  2. 已经打开index.html之后,点击了刷新或者ctrl+r,此时就是普通的刷新。index.html如果没有缓存/或已过期,则会向服务器发起请求。 如果index.html中引用的资源已过期,则这些资源请求也会向服务器发起。没过期的话,在开发人员工具中会看到资源是from disk cache。
    这提醒我们开发者: 为了让用户能够充分利用本地缓存, 我们要把js,css等资源的过期时间设置的很长,比如一年。然后把服务器端的304做好。 也就是给客户端发送资源的时候,不要忘记设置last-modify这种标示头。 因为只有客户端有这种头,在此情景下浏览器才会把这种头再带给服务器,服务器才能验证是否返回304,要是没有这种头,服务器只能返回200重发资源了。

    1
    2
    If-Modified-Since:Tue, 06 Jun 2017 06:04:45 GMT
    If-None-Match: W/"593645fd-1cad"
  3. 用户点击了强制刷新。(比如chrome浏览器在刷新按钮上点击右键,会有一个硬性加载)。
    在这种情况下,浏览器会当做之前这资源从来没有缓存过,此时不会带上任何If-None-Match If-Last-Modify字段,而且会多加一个 Cache-Control: no-cache 以表达自己不希望缓存的情绪。

缓存优化时index.html该怎么办

一般首页又不能设置太大的过期时间。为什么呢,因为首页作为入口通常网站升级后要让用户看到新的功能,此时首页内部负责link新的app.js等地址。而,要想让用户看到新的页面,首页自身必须让用户拿到最新的。

所以,首页index.html最好不能缓存在客户端,而应该不管用户怎么访问首页,都要到服务器请求一下。

百度是怎么干的呢?
百度的服务器是对index.html把cache-control的过期时间设置成0,或private(百度用的private, 更新: 现在用的no-cache),expire设置一个当前时间,也就是告诉浏览器不缓存首页。所以百度的index.html根本没有做304.

不过我认为每次请求归请求,我们服务器端的304还是要做的,所以服务端发送资源给浏览器时要给首页加上etags或者last-modify,这样可以让服务端后续收到请求后决定是否返回304重新利用缓存 (实际上虽然index.html的Cache-Control的max-age=0, 但浏览器确实是缓存了一份index.html的)。

Last-Modified与ETag一起使用时的优先级

如果同时有 etag 和 last-modified 存在,在发送请求的时候会一次性的发送给服务器,没有优先级,服务器会比较这两个信息(在具体实现上,大多数做法针对这种情况只会比对 etag)。服务器在输出上,如果输出了 etag 就没有必要再输出 last-modified(实际上大多数情况会都输出)。

具体可以参见知乎上的这个问题——关于浏览器的缓存,有了Etag,last-Modified 还有必要存在吗???。如果要深究,就要仔细看看 RFC 了

etag和last-modify用哪个比较好呢?

这个主要考虑在负载均衡的情况下,由于请求分发到了不同机器上,如何判断资源有没有变更,就要通过etag更准确了。因为不同服务器的时间可能是不准确的,这就导致文件的修改时间也不准确。

根据知乎的讨论可以发现:

  • etag比last-modify更严谨。因为lm在1秒时间内也可能发生更改;另一种情况是如果资源是动态生成的,那么服务端的动态程序使用etag更能标识这个资源有没有变化
  • last-modify确实会更简便一些,提高304协商缓存时候的性能;但是如果后端是负载均衡的架构,不同机器的时间如果不一致会导致判定不准确。

所以图片等静态文件,由于更改修改时间必然基本上等同于内容的更改,所以使用last-modify比较合适,但可能出现刚刚说的负载均衡时间不一致的问题。而代码或动态生成的内容等资源更适合使用etag,因为他们有时候可能会经常更改修改时间,但内容可能是一样的,etag更能保证缓存协商时的精确性

QA

本文基于自己的实验得出的结论。如有不对之处,请大佬们指出。

Refer

扑朔迷离的etag
前端面试之性能问题
说说浏览器端缓存的那点事儿-扑朔迷离的 etag 与 last-modified