前端知必会的缓存规则

前言

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。

这是缓存的官方定义。缓存从来都是前端的一个痛点,缓存与浏览器版本和 HTTP 版本也有不少关系,本文利用Firefox 59.0.2(64位)进行演示,因为该浏览器可以最大程度契合HTTP缓存的表现,当然文中也会涉及Chrome 65.0.3325.181 (正式版本) (64 位)和Safari等浏览器的特殊性。

正文

缓存的好处

所谓缓存目的就是加速各种静态资源的访问。

  • 减轻服务器端的访问压力(不用每次去请求资源)
  • 加快页面的载入速度,提升用户体验(打开本地资源速度当然比请求回来再打开要快得多)
  • 减少带宽消耗

✨缓存的种类

很多的开发者把cookie、localStorage、sessionStorage以及indexDB存储的数据也称之为缓存,理由是它们都是保存在客户端的数据,其实这是不严谨的,cookie的存在更多的是为了让服务端区别用户,localStorage、sessionStorage以及indexDB则是为了保存大量结构化数据,当然也可以用来缓存js和css等静态资源

宏观

从宏观上来看,缓存可分为私有缓存和共享缓存。

类型定义值说明私有缓存各级代理不能缓存的缓存,仅浏览器设置缓存Cache-Contro:private共享缓存能被各级代理缓存的缓存,表示浏览器和代理服务器都可以设置缓存Cache-Contro:public

上述表提到了浏览器缓存、服务器缓存以及Cache-Control 在下文中都会有更加具体地介绍

微观

从微观角度,缓存可以分为:

  • 浏览器缓存 浏览器缓存相信大家都知道,就是存储在你本地磁盘上资源副本,当用户点击前进或者后退按钮或是再次去访问某个页面的时候能够更快的响应
  • 代理服务器缓存

代理服务器缓存原理和浏览器端类似,但规模要大得多,它可以为多个用户提供缓存,其实就是共享缓存,因为同一个缓存可能会被重用多次

  • 反向代理服务器缓存

反向代理服务器缓存也叫做网关缓存(nginx反向代理、CDN缓存),网关也是一个中间服务器。

一段反向代理服务器缓存的配置:

location / {
 location ~ \.(php|php5)?$ {
 proxy_pass http://www.gao.com;
 proxy_set_header X-RealA-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header Host $Host;
 proxy_redirect off;
 }
 location ~ .*\.(html|htm|gif|jpg|jpeg|bmp|png|ico|txt|js|css)$ {
 root /data/webapps/gao; //静态类资源由本地响应
 expires 3d;
 add_header Static Nginx-Proxy;
 }
 }

说明:代理服务器缓存和反向代理服务器缓存的区别

答:代理服务器存在的目的是用来减轻客户端网络带宽流,具体来说:假设公司架设了一台代理服务器,请求通过代理服务器后,代理服务器将资源缓存下来,供其他用户访问,这样一来,用户发起的请求无需再次穿越Internet,从而降低网络流量,节省资费。(类似浏览器缓存,返回200 from dist cache或者200 from memory cache) (这里有些疑问,不确定来自本地和代理服务器缓存,更新一下,现在已经可以确定,来自代理服务器的缓存,请求仍然会是200,无论是强缓存还是协商缓存都是对于浏览器来说的,如果是200,可能来源于服务端缓存或者真实服务端)

前端知必会的缓存规则

反向代理服务器存在的目的是用来减轻源服务器的负载,而不是用来减轻客户端网络带宽流量,具体来说如果有缓存会返回304状态码,如下图:

前端知必会的缓存规则

补充说明:网上关于200 from dist cache或者200 from memory cache存在两种说法:

Chrome会根据本地内存的使用率来决定缓存存放在哪,如果磁盘使用率很高,放在磁盘里面,内存的使用率很高会暂时放在内存里面


当首次访问网页时,资源文件被缓存在内存中,同时也会在本地磁盘中保留一份副本。当用户刷新页面,如果缓存的资源没有过期,那么直接从内存中读取并加载。当用户关闭页面后,当前页面缓存在内存中的资源被清空。当用户再一次访问页面时,如果资源文件的缓存没有过期,那么将从本地磁盘进行加载并再次缓存到内存之中。

经过测试认为第一种说法更加合理(Chrome验证):

前端知必会的缓存规则

上述两张图来自于百度首页,一个来自于dist cache,一个来自memory cache,两张图的request headers不同是区别两种缓存的方式

  • 数据库缓存

将查询后的数据放到内存中,下一次查询直接从内存中读取

HTTP中和缓存相关的首部字段

1. 通用首部字段

字段名称说明Cache-Control控制缓存具体的行为PragmaHTTP1.0时的遗留字段,当值为"no-cache"时强制验证缓存Date创建报文的日期时间(启发式缓存阶段会用到这个字段)

2. 响应首部字段

字段名称说明ETag服务器生成资源的唯一标识Vary代理服务器缓存的管理信息Age资源在缓存代理中存贮的时长(取决于max-age和s-maxage的大小)

3. 请求首部字段

字段名称说明If-Match携带上一次请求中资源的ETag,比较文件是否有新的修改If-None-Match携带上一次请求中资源的ETag,比较文件是否没有新的修改If-Modified-Since携带上一次请求中资源的Last-Modified,比较资源前后两次访问最后的修改时间是否一致If-Unmodified-Since携带上一次请求中资源的Last-Modified,比较资源前后两次访问最后的修改时间是否不一致

4. 实体首部字段

字段名称说明ExpiresHTTP1.0时的遗留字段,实体主体的过期时间Last-Modified资源最后一次修改的时间

首部字段的取值详解

前端知必会的缓存规则

图来自《图解HTTP》一书,再根据上述的描述我们知道,请求首部和响应首部的Cache-Control都与缓存有关,一个是从客户端角度,一个是从服务端角度

1. Cache-Control

缓存请求指令

指令参数说明no-cache会进行缓存,但是需要强制源服务器再次验证no-store不缓存请求或是响应的任何内容max-age=单位是秒缓存的时长,也是响应的最大的Age值,当max-age=0时,效果等同于no-cachemin-fresh=在指定时间内,响应仍然有效no-transform代理不可更改媒体类型only-if-cached从缓存获取cache-extension新的指令标记(token)

缓存响应指令

指令参数说明public任意一方都能缓存该资源(客户端、代理服务器等)private只能特定用户缓存该资源no-cache缓存前必须先确认其有效性no-store不缓存请求或响应的任何内容max-age=单位是秒缓存的时长,也是响应的最大的Age值,当max-age=0时,效果等同于no-caches-maxage=公共缓存服务器响应的最大Age值must-revalidate可缓存但必须再向源服务器进确认proxy-revalidate要求中间缓存服务器对缓存的响应有效性再进行确认no-transform代理不可更改媒体类型cache-extension新的指令标记(token)

public和private指令

这两个指令只在响应指令中出现,public表示所有的代理服务器和客户端均可缓存响应,假设我们都走的公司代理服务器,公司可以缓存一些资源,我们就可以都访问到该缓存,而private只针对特定用户缓存

no-cache指令

前端知必会的缓存规则

2. Expires

如同时存在max-age 和 Expires ,当应用为HTTP/1.1版本时,会忽略Expires首部字段,当HTTP/1.0版本时,会忽略max-age

3. Pragma

这是HTTP/1.0里面的一个字段,优先级高于Cache-Control和Expires一般会这么使用

但是事实上这种禁用缓存的形式用处很有限:

  1. 仅有IE才能识别这段meta标签含义,其它主流浏览器仅能识别Cache-Control: no-store的meta标签。
  2. 在IE中识别到该meta标签含义,并不一定会在请求字段加上Pragma,但的确会让当前页面每次都发新请求

上述提到的都是浏览进行缓存表现的字段,下面说一说校验字段

4. Last-Modified和If-Modified-Since

服务器可以通过Last-Modified响应首部来指明当前访问资源最近一次修改是在什么时候,客户端在发送请求校验资源是否发生修改时,可以通过If-Modified-Since请求首部向服务器端确认资源在给定时间点之后是否发生更改,服务器可以根据比对资源的最近一次修改时间和If-Modified-Since首部指定的时间来决定返回304状态码还是返回新的资源。

5. ETag和If-Match

使用上述机制存在的问题就是:

  1. 因为Last-Modified指定的修改时间是精确到秒级的,如果服务器上的文件在一秒内发生多次更改,单纯依靠文件修改时间就检测不到文件已经发生变化
  2. 如果文件在某个时间段内发生多次更改,但是前后文件内容并没有发生变化,这时依靠文件修改时间去判断文件已经发生修改也不合适

因此产生了ETag和If-Match。

通过Etag响应首部就能够解决上面提到的问题。通过对当前文件内容计算生成一个唯一标识,通过比对标识是否相同来判断文件内容是否发生变化。相比于比较文件修改时间,这种方式更加有效。客户端在发送条件请求时,会包含 If-None-Match 请求首部,来判断文件内容是否发生变化。

6. Date

最后介绍一个响应头中的Date字段,该字段是区分缓存来自源服务器还是缓存服务器,如果多次刷新Date一直变化,说明缓存来自源服务器,否则来自缓存服务器

️浏览器的缓存策略

浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的。 在前一部分中,介绍了关于缓存的主要字段,而这些字段与浏览器的缓存策略息息相关。

强缓存阶段

强缓存主要是采用响应头中的Cache-Control和Expires两个字段进行控制的,如同时存在max-age 和 Expires ,当应用为HTTP/1.1版本时,会忽略Expires首部字段,当HTTP/1.0版本时,会忽略max-age,由于Expires是HTTP1.0时代的产物,因此设计之初就存在着一些缺陷,如果本地时间和服务器时间相差太大,就会导致缓存错乱。

客户端通过对比本地时间和服务器生存时间来检测缓存是否可用,如果可用,客户端会直接使用缓存,如果不可用,会进入协商缓存阶段。

协商缓存阶段

强缓存机制如果检测到缓存失效,就需要进行服务器再验证。这种缓存机制也称作协商缓存。

浏览器在第一次获取请求的时候,就会在响应头中携带上资源的上次服务器修改日期(Last-Modified)或者资源的标签(Etag)。后续的请求服务器会根据请求头上的If-Modified-Since(对应Last-Modified)和(If-None-Match)字段来判断资源是否失效,一旦资源过期,则服务器会重新发送新的资源到客户端上,从而保证资源的有效性。

需要注意的是当响应头中同时存在Etag和Last-Modified的时候,会先对Etag进行比对,随后才是Last-Modified。

启发式缓存阶段

有一种特殊的响应头类似这样

Age:23146
Cache-Control: public
Date:Tue, 28 Nov 2017 12:26:41 GMT
Last-Modified:Tue, 28 Nov 2017 05:14:02 GMT
Vary:Accept-Encoding

如果没有Expires或max-age确定缓存过期时间的字段,浏览器会默认根据响应头中2个时间字段 Date 和 Last-Modified 之间的时间差值,取其值的10%作为缓存时间周期 这就是启发式缓存阶段

用户行为与缓存

操作说明打开新窗口如果指定cache-control的值为private、no-cache、must-revalidate,那么打开新窗口访问时都会重新访问服务器。而如果指定了max-age值,那么在此值内的时间里就不会重新访问服务器,例如:Cache-control: max-age=5 表示当访问此网页后的5秒内不会去再次访问服务器.在地址栏回车如果值为private或must-revalidate,则只有第一次访问时会访问服务器,以后就不再访问。如果值为no-cache,那么每次都会访问。如果值为max-age,则在过期之前不会重复访问。按后退按扭如果值为private、must-revalidate、max-age,则不会重访问,而如果为no-cache,则每次都重复访问.按刷新按扭无论为何值,都会重复访问.(可能返回状态码:200、304,这个不同浏览器处理是不一样的,FireFox正常,Chrome则会启用缓存(200 from cache))按强制刷新按钮重新请求

浏览器行为

对比一:

上述说明过,Firefox 59.0.2(64位)下的http缓存行为正常

前端知必会的缓存规则

如上图,刷新页面,浏览器会带上Cache-Control: max-age=0,因此返回304。

前端知必会的缓存规则

但是Chrome 65.0.3325.181 (正式版本) (64 位)的刷新却不会带上Cache-Control: max-age=0的请求头,打开百度会发现无论如何点击刷新按钮,依然返回的是200。

另外这个奇怪的情况在不同的网站甚至不同的电脑下出现频率都不一致,所以暂时将其归咎于浏览器的怪异反应。

那么有这么一个问题——是否有办法在浏览器点击“刷新”按钮的时候不让浏览器去发新的验证请求呢?

$(window).load(function() {
 var bg='http://img.infinitynewtab.com/wallpaper/100.jpg';
 setTimeout(function() { //setTimeout是必须的
 $('#bgOut').css('background-image', 'url('+bg+')');
 },0);
});

来源知乎

对比二:

在这里服务端的max-age=0,新建标签页时Firefox的表现:

前端知必会的缓存规则

新建标签页时Chrome的表现:

前端知必会的缓存规则

经测试发现,Chrome没有按照预期返回304状态码,而且Chrome有的时候返回200 from disk cache有时返回200,所以暂时将其归咎于浏览器的怪异反应。

怪异现象的思考

由于chrome的现象,便在网上查找chrome缓存资料

下述一段回答来自stackoverflow,

Chrome's cache is a database, and that database is also compressed to save space. When you retrieve a document from cache, the price of that retrieval is not zero. Chrome has to look up the item in the cache database, and then inflate that entry into memory so Chrome can work with it. I don't know the exact details concerning how the Network chrome-dev-tools panel shows the times, but I would guess that getting that file from disk, uncompressing it, and then parsing and working with the result is what you're seeing reflected in "Time downloaded."

大致的意思是:谷歌浏览器的缓存是一个数据库,它每次从缓存中加载数据时,实际是在数据库中找到索引,如果发现有变化,将其下载出来,但是对外展示200 from disk cache

一个小demo

测试cache-control

max-age=0时:

后端代码:

'use strict';
var express = require('express'),
	app = express();
app.use(express.static("./", {
	maxAge: 0
}));
app.listen(4000);

max-age=0前端代码:



	
	 


	

 

看看Firefox 59.0.2(64位)下的http缓存行为

前端知必会的缓存规则

前端知必会的缓存规则

看看Chrome 65.0.3325.181 (正式版本) (64 位)下的http缓存行为

前端知必会的缓存规则

前端知必会的缓存规则

新建标签页的行为更加诡异,会直接返回200 from disk cache 但是我如果修改了部分代码,即使返回200 from disk cache也会展示出来更新,这是浏览器内部机制,没有详细了解,感兴趣可以自己查阅资料

max-age=10时:

  1. Firefox刷新会带上Cache-Control:max-age=0进行协商缓存校验,回车或者新建标签页在10s内会返回200 from cache,10s之后会进行协商缓存校验
  2. Chrome刷新或者回车在10s内返回200 from cache,10s之后会进行协商缓存校验,新建标签页则会返回200 from disk cache

总结:

  1. 当强缓存失效时,一定需要带上协商缓存的字段进行校验
  2. 如果浏览器请求头中携带Cache-control:max-age=0,那么浏览器会带上协商缓存的字段进行校验

实际使用

考虑缓存的内容:

  • css样式文件
  • js文件
  • logo、图标
  • html文件
  • 可以下载的内容

不经常改变的文件:

给 max-age 设置一个较大的值,一般设置 max-age=31536000

可能经常需要变动的文件:

Cache-Control: no-cache / max-age=0 比如入口 index.html 文件、文件内容改变但名称不变的资源。选择 ETag 或 Last-Modified 来做验证,进行协商缓存校验


分享到:


相關文章: