前言
缓存对页面性能的提升是众多优化手段中见效最明显的,也是面试中经常会问到的高频题,缓存再实际项目中如何应用的? 大型项目中又是如何使用缓存的?
本文将讨论的缓存分为两种,一种是跟后端交互通过http字段头配置的缓存(强制缓存,协商缓存)这种能加快资源再次防问的速度,另一种是能加快资源首次防问速度的缓存(CDN,DNS),并对此给出一套解决方案。
协商缓存
协商两字指的是每次请求这个资源时,都问一下服务器(协商一下)有没有更新过,如果有更新那么就取服务器最新的资源,否则就返回一个304状态码告诉浏览资源没有更新过表示取本地缓存中的资源即可。
可见这种方式是由服务器决定缓存内容是否失效。
实现细节
前后端交互中协商缓存使用两组字段进行控制Last-Modified & If-Modified-Since
,Etag & If-None-Match
,后者的优先级高于前者。
Last-Modified & If-Modified-Since
-
请求一个新资源时,服务器会往响应头写入
Last-Modified
字段,格式是时间。 -
浏览器将资源和
Last-Modified
字段保存在本地。 -
下次请求这个资源时往请求头中添加
If-Modified-Since
字段,字段值是上次保存的Last-Modified
。 -
服务器收到
If-Modified-Since
这个字段跟资源的最后修改时间进行对比。 -
如一致则返回304表示没修改过取本地资源即可,否则返回200并将最新的资源反回给浏览器。
Etag & If-None-Match
Last-Modified & If-Modified-Since
这种方式有个缺陷,因为它们是通过时间来对比文件是否更新的,如果资源更新的速度是秒以下单位,那么该缓存是不能被使用的,因为它的时间单位最低是秒,而Etag & If-None-Match
是通过文件内容计算出来的hash值确定文件是否更新的。
两种方式只是对比文件更新方式上有所不同,其它逻辑一致,Etag
对应的是Last-Modified
,If-None-Match
对应的是If-Modified-Since
。
强制缓存
所谓强制缓存就是当请求到一个静态资源,我们给它设置一个过期时间,下次再请求这个资源时进行对比,如果当前时间小于设置的过期时间,那么就使用缓存,这种方式相对协商缓存,直接减少了http请求。
实现细节
前后端交互中强制缓存使用的是Expire
、Cache-Control
两个这段进行控制,前者是http1.0的标准,后者http1.1的标准,前者已逐渐被后者代替。
Expire
Expires: Thu, 11 Nov 2020 00:00:00 GMT
浏览器请求一个新资源时,服务给响应头设置这么一段,表示2020年11月11号之前请求这个文件都使用缓存中的,这个字段有个缺点,因为它是在服务器设置的,如果客户端的时间跟服务器的时间不一致那么缓存生效时间就不精准,因为它设置的是绝对时间。
Cache-Control
这个属性具有多个可选参数,参数不区分在小写,多个参数以逗号分隔,以下是常用的几个参数:
-
public
表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等) -
private
表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它),私有缓存可以缓存响应内容,比如:对应用户的本地浏览器。 -
no-cache
在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证(协商缓存验证)。 -
no-store
缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存。 -
max-age
设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间。
以上属性从MDN复制来的,要查看更多属性点这里!
我们来看下一个最重要的属性 max-age
它与 Expire
一样都是设置过期时间,但前者设置的是相对时间,就不会有客户端跟服务器时间不一致而产生的bug了:
Cache-Control:public, max-age=31536000
实际项目中怎么用?
直观上可以看到强制缓存比协商缓存性能要更好,为什么? 因为协商缓存每次都要发送一个http请求询问服务器,而强制缓存一但缓存了就再也不向服务器请求这个资源了。
等等 … 再也不请求了 ???
那服务器的资源要是更新了怎么办 ?
举个例子,如果服务器的API更新了一个版本,前端资源也进行了相应的更新,但是使用了强制缓存就没办法了,服务器更新了资源前端还是取本地缓存的,那缓存文件里用的还是旧版本的API岂不是炸了? (当然这只是个例子,实际开发中服务器API肯定会向后前兼容的,比如说发布了新版本地址是 v2 /xxx 老版本地址还是 v1/xxx)。
想像一下之所以每次都会使用本地缓存中的资源是不是因为浏览器根据这个资源名,在本地缓存中找到了对应名字的资源了?
那我把资源的名字改掉它不就在缓存中找不到了吗? 没错如果服务的资源更新了就重新换个名字,那我们前端就不会再到缓存中去取了。
通过上图可以看到名字变了就不会取缓存中的了,但是a.html不能使用强制缓存,因为a.html是入口文件,入口文件的文件名变了别人就防问不到你的页面了。
我们可以总结出: 入口文件使用协商缓存,页面内的资源文件使用强制缓存。
webpack
上文说到了资源文件更新后每次都换一个新名字,这个名字是根据文件内容的摘要信息计算出来的一个hash值,文件内容变了名字就变,前端看到名字变了就请求服务器,没变就取缓存,这样挺好的.
但你有没有想过这样一个场景 A.CSS
依赖了 B.CSS
, 而B.CSS
又依赖了 C.CSS
,当
C.CSS
文件内容发生改变C.CSS
文件名就变了,那么B引用了C,B的内容变了B的文件名也要变,A同理。
这种操作不可能手工去改,所以就引出了我们的神器 webpack
,使用webpack可以自动帮我们生成文件hash处理依赖关系。
CDN
前面介绍的缓存策略只能加快资源的再次防问速度,使用CDN可以加快我们首次防问资源的速度,CDN全称Content delivery networks(内容分发网络),其目的是通过在源服务器和用户之间增加一层新的网络架构,将网站的内容分发到最接近用户的网络“边缘”,使用户可以就近取得所需的内容,提高用户访问网站的响应速度。
说的是通过在各地部署很多缓存服务器,然后将你的请求指派到最近的缓存服务器上,从而提高防问速度,类似于你取快递时的快递代收点,你总是会去离你最近的代收点去取快递。
下图说明使用CDN前跟没使用CDN前的区别:
可以看到有CDN的情况下北京的用户直接将请求发给了离自己最近的武汉服务器,而没有CDN则发给了香港的源服务器。
我们实际开发中会把静态资源放到CDN服务器(这里指的不是将静态资源上传上去,而是将静态资源的地址改成cdn服务器的地址,让cdn去找到源服务器取这个文件,取到了就缓存下来),这些静态资源包括js、css、img等等… 但是页面的入口文件url不能改成cdn服务器的, 因为如果入口文件被缓存了,那么页面就更新不了了。
总结
实际工作中 ,我们将协商缓存强制缓存结合使用,将静态资源使用强制缓存,页面入口文件使用协商缓存,强制缓存通过文件摘要生成hash当文件更新文件名也更新解决服务器更新本地仍使用旧资源的情况,使用webpack为我们做这部分工作,在些基础上还可以部署cdn服务,将静态资源使用一个单独域名指向dns服务器,dns再指向你的源服务器,通过dns来缓存提高首次防问速度。