近在项目中遇到了 IE浏览器因缓存问题未能成功向后端发送 GET类型请求 的bug,然后顺藤摸瓜顺便看了看缓存的知识,觉得有必要总结跟大家分享一下。
在前端开发中,性能一直都是被大家所重视的一点,然而判断一个网站的性能最直观的就是看网页打开的速度。其中提高网页反应速度的一个方式就是使用缓存。一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。
1. 介绍
web缓存是指一个web资源(如html页面,图片,js,数据等)存在于web服务器和客户端(浏览器)之间的副本。
缓存会根据进来的请求保存输出内容的副本;当下一个请求来到的时候,如果是相同的URL,缓存会根据缓存机制决定是直接使用副本响应访问请求,还是向源服务器再次发送请求。比较常见的就是浏览器会缓存访问过网站的网页,当再次访问这个URL地址的时候,如果网页没有更新,就不会再次下载网页,而是直接使用本地缓存的网页。只有当网站明确标识资源已经更新,浏览器才会再次下载网页。至于浏览器和网站服务器是如何标识网站页面是否更新的机制,将在后面介绍。
1.1 web缓存的作用
web缓存的作用显而易见:
1.2 web缓存的类型
web缓存大致可以分为以下几种类型 详细内容:
浏览器通过代理服务器向源服务器发起请求的原理如下图:
浏览器先向代理服务器发起web请求,再将请求转发到源服务器。它属于共享缓存,所以很多地方都可以使用其缓存资源,因此对于节省流量有很大作用。
浏览器缓存是将文件保存在客户端,在同一个会话过程中会检查缓存的副本是否足够新,在后退网页时,访问过的资源可以从浏览器缓存中拿出使用。通过减少服务器处理请求的数量,用户将获得更快的体验
下面着重关注一下浏览器缓存。
2. web缓存的工作原理
所有的缓存都是基于一套规则来帮助他们决定什么时候使用缓存中的副本提供服务(假设有副本可用的情况下,未被销毁回收或者未被删除修改)。这些规则有的在协议中有定义(如HTTP协议1.0和1.1),有的则是由缓存的管理员设置(如DBA、浏览器的用户、代理服务器管理员或者应用开发者)。
2.1 浏览器端的缓存规则
对于浏览器端的缓存来讲,这些规则是在HTTP协议头和HTML页面的 Meta标签中定义的。他们分别从新鲜度和校验值两个维度来规定浏览器是直接使用缓存中的副本,还是需要去源服务器获取更新的版本。
2.2 浏览器缓存的控制
2.2.1 使用HTML的 Meta 标签
< META HTTP - EQUIV = "Pragma" CONTENT = "no-cache" >
上述代码的作用是告诉浏览器当前页面不被缓存,每次访问都需要去服务器拉取。使用上很简单,但只有部分浏览器可以支持,而且所有缓存代理服务器都不支持,因为代理不解析HTML内容本身。可以通过这个页面测试你的浏览器是否支持:[Pragma No-Cache Test] (http://www.procata.com/cachetest/tests/pragma/index.php)。
2.2.2 使用缓存有关的HTTP消息报头
一个URI的完整HTTP协议交互过程是由HTTP请求和HTTP响应组成的。有关HTTP详细内容可参考《Hypertext Transfer Protocol — HTTP/1.1》、《HTTP协议详解》等。
在HTTP请求和响应的消息报头中,常见的与缓存有关的消息报头有:
稍微解释一下:
1. Cache-Control
cache-control的种类这么多,然而怎么使用它们呢,参看下图:
2. Expires
缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间 ,需要和Last-modified结合使用。但在上面我们提到过,cache-control的优先级更高。Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
3. Last-modified & If-modified-since
服务器端文件的最后修改时间,需要和cache-control共同使用,是检查服务器端资源是否更新的一种方式。当浏览器再次进行请求时,会向服务器传送If-Modified-Since报头,询问Last-Modified时间点之后资源是否被修改过。如果没有修改,则返回码为304,使用缓存;如果修改过,则再次去服务器请求资源,返回码和首次请求相同为200,资源为服务器最新资源。
4. Etag & & If-None-Match
根据实体内容生成一段hash字符串,标识资源的状态,由服务端产生。浏览器会将这串字符串传回服务器,验证资源是否已经修改,如果没有修改,过程如下:
2.2.3 缓存报头种类与优先级
1. Cache-Control与Expires
Cache-Control与 Expires的作用一致,都是指明当前资源的有效期,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。只不过 Cache-Control的选择更多,设置更细致,如果同时设置的话,其优先级高于 Expires。
2. Last-Modified与ETag
你可能会觉得使用 Last-Modified 已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要 Etag(实体标识)呢?HTTP1.1中Etag的出现主要是为了解决几个 Last-Modified 比较难解决的问题:
Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。Etag的服务器生成规则和强弱Etag的相关内容可以参考,《互动百科-Etag》和《HTTP Header definition》,这里不再深入。
3. Last-Modified/ETag 与 Cache-Control/Expires
配置 Last-Modified/ETag的情况下,浏览器再次访问统一URI的资源,还是会发送请求到服务器询问文件是否已经修改,如果没有,服务器会只发送一个304回给浏览器,告诉浏览器直接从自己本地的缓存取数据;如果修改过那就整个数据重新发给浏览器;
Cache-Control/Expires则不同,如果检测到本地的缓存还是有效的时间范围内,浏览器直接使用本地副本,不会发送任何请求。两者一起使用时, Cache-Control/Expires的优先级要高,即当本地副本根据 Cache-Control/Expires发现还在有效期内时,则不会再次发送请求去服务器询问修改时间 Last-Modified或实体标识 Etag了。
一般情况下,两者会配合一起使用,因为即使服务器设置缓存时间, 当用户点击“刷新”按钮时,浏览器会忽略缓存继续向服务器发送请求,这时 Last-Modified/ETag将能够很好利用304,从而减少响应开销。
2.2.4 哪些请求不能被缓存?
无法被浏览器缓存的请求:
3. 使用缓存流程
一个用户发起一个静态资源请求的时候,浏览器会通过以下几步来获取并展示资源:
缓存行为主要由缓存策略决定,而缓存策略由内容拥有者设置。这些策略主要通过特定的HTTP头部来清晰地表达。
以上过程也可以被概括为三个阶段:
4. 用户操作行为与缓存的关系
用户在使用浏览器的时候,会有各种操作,比如输入地址后回车,按F5刷新等,这些行为会对缓存有什么影响呢?
通过上表我们可以看到,当用户在按 F5进行刷新的时候,会忽略Expires/Cache-Control的设置,会再次发送请求去服务器请求,而Last-Modified/Etag还是有效的,服务器会根据情况判断返回304还是200;
而当用户使用 Ctrl+F5进行强制刷新的时候,只是所有的缓存机制都将失效,重新从服务器拉去资源。
5. 如何从缓存角度改善站点
关注微信公众号:安徽思恒信息科技有限公司,了解更多技术内容……
近一次移动端Vue应用的上线,导致某些用户使用某些功能时出现问题,经主动清空缓存后恢复。有时候清空微信应用的存储空间缓存仍不能解决问题,此时安卓机可借助微信TBS调试工具 http://debugx5.qq.com (微信中打开页面,勾选最下面四个选项清除缓存),但该工具目前只支持安卓手机,苹果机就比较麻烦了。为了找到问题的本质,从根本上避免问题,最近浏览了一些文章,其中有一篇对浏览器缓存的分析及在Nginx中对应的处理策略总结的比较好,这里分享给大家。
以下为原文。
关于http或者是浏览器缓存策略,我认为可以分为这三种:
有时,我们希望浏览器永远都不要使用缓存,全部到服务器拉取数据,此时即为不使用缓存,我们可以在服务端通过Cache-Control为 no-store实现。
服务器端针对上面文件设置了no-store,可以看到在请求的时候,无论怎么刷新,都是返回200,不会显示304,也不会显示“memory cache”或“disk cache”,说明真的都是从服务器重新拉取数据。
比如我们想设置html文件不缓存,可以在域名的解析配置中如下设置,当文件后缀为html或htm时add_header Cache-Control "no-store"
server {
listen 80;
server_name yourdomain.com;
location / {
try_files $uri $uri/ /index.html;
root /yourdir/;
index index.html index.htm;
if ($request_filename ~* .*\.(?:htm|html)$)
{
add_header Cache-Control "no-store"; //对html文件设置永远不缓存
}
}
}
这种方式缺点就是每次都要去服务端拉取文件,即使文件没有更新,很明显这样增加了不必要的带宽消耗。
如果文件没有更新,我们就使用缓存,只有更新了才去拉取最新文件,这样多好,这就是协商缓存。
协商缓存就是浏览器携带文件缓存标识(如Last-Modified或ETag),向服务器发送请求,由服务器根据文件缓存标识来决定是否使用缓存,如果文件没有更新,则告诉浏览器使用本地缓存,如果文件更新了,则直接返回新文件内容。
可以看出,相比不使用缓存,协商缓存是会大大减少带宽消耗的。
我们在浏览器调试页面,可以看到有304的,即是使用了协商缓存
服务器返回的header中会有Last-Modified和ETag标识,而浏览器请求header中会包含If-Modified-Since和If-None-Match
在 http 1.0 版本中,第一次请求资源时服务器通过 Last-Modified 来设置响应头的缓存标识,并且把资源最后修改的时间作为值填入,然后将资源返回给浏览器。在第二次请求时,浏览器会首先带上 If-Modified-Since 请求头去访问服务器,服务器会将 If-Modified-Since 中携带的时间与资源修改的时间匹配,如果时间不一致,服务器会返回新的资源,并且将 Last-Modified 值更新,作为响应头返回给浏览器。如果时间一致,表示资源没有更新,服务器返回 304 状态码,浏览器拿到响应状态码后从本地缓存数据库中读取缓存资源。
这种方式有2个弊端,第一个就是当服务器中的资源增加了一个字符,后来又把这个字符删掉,本身资源文件并没有发生变化,但修改时间发生了变化。当下次请求过来时,服务器也会把这个本来没有变化的资源重新返回给浏览器;第二个就是修改时间的单位为秒,所以存在1s的间隙,即使更新了,也会认为没有更新。
在 http 1.1 版本中,服务器通过 Etag 来设置响应头缓存标识。Etag 的值由服务端生成,可以认为是文件内容的hash值。在第一次请求时,服务器会将资源和 Etag 一并返回给浏览器,浏览器将两者缓存到本地缓存数据库。在第二次请求时,浏览器会将 Etag 信息放到 If-None-Match 请求头去访问服务器,服务器收到请求后,会将服务器中的文件标识与浏览器发来的标识进行对比,如果不相同,服务器返回更新的资源和新的 Etag ,如果相同,服务器返回 304 状态码,浏览器读取缓存。
可以在服务端通过设置Cache-Control为 no-cache或者max-age=0来实现
有时我们希望文件强制使用缓存,比如通过vue-cli产生的js和css,文件名上带有hash值,所以如果文件名没有变的时候,我们希望文件永久缓存,这样可以减少网络请求。
强制缓存整体流程比较简单,就是在第一次访问服务器取到数据之后,在过期时间之内不会再去重复请求。实现这个流程的核心就是如何知道当前时间是否超过了过期时间。
强制缓存的过期时间通过第一次访问服务器时返回的响应头获取。在 http 1.0 和 http 1.1 版本中通过不同的响应头字段实现。
在 http 1.0 版本中,强制缓存通过 Expires 响应头来实现。 expires 表示未来资源会过期的时间。也就是说,当发起请求的时间超过了 expires 设定的时间,即表示资源缓存时间到期,会发送请求到服务器重新获取资源。而如果发起请求的时间在 expires 限定的时间之内,浏览器会直接读取本地缓存数据库中的信息(from memory or from disk),两种方式根据浏览器的策略随机获取。
在 http 1.1 版本中,可以设置Cache-Control中的 max-age=xxx ,来表示缓存的资源将在 xxx 秒后过期。一般来说,为了兼容,两个版本的强制缓存都会被实现。
为什么有了Expires,后来又增加了max-age呢,这是因为Expires是一个绝对时间,有可能客户端的时间和服务器不一致,导致缓存不能按照预期进行,而max-age则是个相对时间,比如3600s,自浏览器请求后3600s之内,都使用本地缓存,和客户端的时间没关系。
由于打包后的js、css和图片,一般名称都带有hash值,名称中的hash变了,自然会拉取新文件,所以我们可以将这类文件设置为强制缓存,只要文件名不变,就一直缓存,比如缓存100天或者一年。
而html文件则不能设为强制缓存,一般html名称是没法带hash值的,所以html如果设置了强制缓存,则永远也没法更新,html不更新,其引用的js、css等名称也不会更新,则整个服务都没有更新,只能让用户清除缓存了。所以针对html文件,我们可以设置协商缓存或者直接不使用缓存,本身html文件都比较小,我是直接使用了不缓存,nginx配置如下。
server {
listen 80;
server_name yourdomain.com;
location / {
try_files $uri $uri/ /index.html;
root /yourdir/;
index index.html index.htm;
if ($request_filename ~* .*\.(js|css|woff|png|jpg|jpeg)$)
{
expires 100d; //js、css、图片缓存100天
#add_header Cache-Control "max-age = 8640000"; //或者设置max-age
}
if ($request_filename ~* .*\.(?:htm|html)$)
{
add_header Cache-Control "no-store"; //html不缓存
}
}
}
欢迎微信搜索关注公众号:半路雨歌,查看更多技术干货文章
缓存,这个问题在面试中出现的频率还是很高的,也是我们日常工作中必知必会的。
所以在本文中,我们来尝试对HTTP 协议中规定的很多请求头和响应头来做总结和归纳,让大家了解到浏览器是如何通过他们来控制缓存的。
下图是一个经典的 GET 请求的处理过程:
当一个请求达到时,浏览器会先检查被访问的资源是否已被缓存。
如果未被缓存(缓存未命中 cache miss),则将请求转发给原始服务器。
如果已被缓存(缓存命中,cache hit),则会检查缓存是否足够新鲜。
如果缓存的副本足够新鲜,则直接将副本返回给客户端,否则会向服务端发起新鲜度验证(revalidation)。如果发现与服务端文件一致,则将本地缓存副本返回给客户端,否则将请求转发给原始服务器。
在这个过程中,由缓存提供的服务在所有请求占比中的比例称为缓存命中率(cache hit rat)。
这种描述方式只能描述请求级别的命中情况,无法体现具体有多少流量来自缓存。
比如一个访问频次很低,尺寸又很大的文件,如果以该命中率来描述的话,命中率非常低。但是这个文件却占据了绝大多数的访问流量。
因此还需要另一个命中率指标来描述,那就是字节命中率(byte hit rate)。字节命中率表示的是缓存提供的字节在所有传输字节中的占比。
浏览器常用校验缓存的机制有两种:
下面我们来分别介绍下这两种机制。
HTTP 通过 Cache-Control:max-age 和 Expires 这两个响应头信息, 让原始服务器向每个文档附加一个“过期日期”。在缓存文档过期之前,缓存可以以任意频次使用这些副本,而无需与服务端联系。
Expires 首部与 Cache-Control:max-age 首部本质上是一样的,区别是 Expires 是 HTTP/1.0 协议规定的首部,且首部取值为一个绝对时间,在这个时间之后缓存失效;Cache-Control:max-age 是 HTTP/1.1 协议规定的首部,且首部取值是一个相对时间,单位为秒。
一个绝对,一个相对。
HTTP 定义了 5 个条件请求首部来完成服务器再验证:
其中最有用的是 If-Modified-Since 和 If-None-Match 两个首部。
一、If-Modified-Since: Date 再验证
If-Modified-Since: Date 再验证请求流程分两种:
1、自指定日期后,文档被修改了,If-Modified-Since 条件为真,GET 请求就会执行。携带新首部的新文档会被返回给缓存,新首部除了其他信息以外,还包含了一个新的过期日期。
2、自指定日期后,文档没有被修改过,If-Modified-Since 条件为假,则会向客户端返回一个304 Not Modified 响应报文,为了提高有效性,一般会发送一个新的过期日期,不会返回文档的主体。
If-Modified-Since 请求首部通常与 Last-Modified 服务器响应首部配合工作。原始服务器会将最后的修改日期附加到文档上去。当浏览器要对已缓存的文档进行再验证时,就会包含一个 If-Modified-Since首部,其中携带有最后修改已缓存副本的日期:
If-Modified-Since: <cached last-modified date>
这里以W3C网站为例:
从上面两张图上,可以分别看到请求头信息中的 If-Modified-Since 和响应头信息中的 Last-Modified。
说白了,就是本地记录的修改日期,与服务器那边记录的修改日期做对比。如果服务的比较新,就重新过去,否则加载本地的。
上图我第一次进来,因为本地并没有If-Modified-Since 记录,所以只能看到响应头中有最后修改时间信息,以及返回的200状态码。
当我再次刷新网页之后:
瞧,请求头也有信息了。因为If-Modified-Since 和 Last-Modified两个值是一样的,所以服务器校验本页面为缓存,返回304状态码。
在某些情况下,If-Modified-Since: Date 可能无法很好地解决缓存问题。
比如一个被周期性复写的文件,虽然修改日期每次都有变化,但是文件的内容往往是一样的,如果此时还根据最后修改时间去判定是否为缓存,显示是不合适的。
这种情况下,就需要借助实体标签(Etag)验证了。
实体标签就是“版本标识符”,是附加到文档上的任意标签(引用字符串),可能包含了文档的序列号或版本名,或者是文档内容的校验信息。
If-None-Match: etag 实体标签验证的工作过程与 If-Modified-Since: Date 再验证的工作过程基本一致,不同的是,服务器会在响应中附加一个 Etag 响应头。当缓存要对已缓存的文档进行再验证时,就会将这个 etag 放到 If-None-Match 请求头中去。
还是以上面的请求为例:
可以看到,If-none-match 在chrome网络面板中的位置,就是在 If-modifed-since 后面,只是它对应的不是修改日期,而是上面相应的etag罢了。
说简单点,就是hash字符串校验。
注意:如果你是第一次访问该页面,请求头中同样没有 if-none-match 信息。
一、服务器端响应
服务端通过Cache-Control 来对响应缓存做限制,以下控制优先级按顺序依次递减:
Cache-Control: no-store 禁止缓存对响应进行复制。
Cache-Control: no-cache/ Pragma: no-cache 缓存可以复制响应,但是在与原始服务器进行新鲜度再验证之前不能将其提供给客户端。Pramga: no-cache 为了兼容 HTTP/1.0,优先级低于 Cache-Control: no-cache。
Cache-Control: must-revalidate 在事前没有跟原始服务器进行再验证的情况下,缓存不能提供缓存副本。
Cache-Control: max-age max-age 指定的秒数内有效。max-age 为零时,不可缓存。
Expires: Date 在实际的绝对日期之前有效。
二、客户端请求
客户端同样可以通过 Cache-Control 请求首部来强化或放松对过期时间的限制。以下是使用时的具体参数及说明:
Cache-Control: max-stale=< s > 缓存可以随意提供副本,如果指定的秒数,那么在这段时间内,文档不能过期。
Cache-Control: min-fresh=< s > 至少在未来< s >秒内文档保持新鲜。
Cache-Control: max-age=< s > 缓存无法返回缓存时间超过< s >的文档。如果与 max-stale 通用,max-stale 优先级更高。
Cache-Control: no-cache/Pragma: no-cache 除非进行了再验证,否则客户端不接受已缓存的资源。
Cache-Control: no-store 缓存应该删除本地缓存副本,使用原始服务器响应。
Cache-Control: only-if-cache 只有当缓存中有副本存在时,客户端才会获取一份副本。
好了,缓存就暂时先说到这里。最后让我们再来看下浏览器的缓存执行流程,巩固一下吧。
浏览器首次请求:
浏览器再次请求时:
*请认真填写需求信息,我们会在24小时内与您取得联系。