无论在工作中还是平时的学习面试过程中,HTTP缓存几乎都是我们绕不开的话题,面对这些常见的知识点,我们不应该选择逃避,而是勇于面对,去搞懂它们。
为什么需要缓存?
在任何一个前端项目中,访问服务器获取数据都是很常见的事情,如果相同的数据被重复请求了不止一次,那么多余的请求必然会浪费网络带宽,以及延迟浏览器渲染所要处理的内容,从而影响用户的使用体验。如果用户使用的是按量计费的方式访问网络,多余的请求还会隐形的增加用户的网络流量资费。因此考虑使用缓存技术对已经获取的资源进行重用,是一种提升网站性能与用户体验的有效策略。
缓存的原理
缓存的原理是在首次请求后保存一份请求资源的响应副本,当用户再次发起相同请求后,如果判断缓存命中则拦截请求,将之前存储的响应副本返回给用户,从而避免了重新向服务器发起资源请求。
HTTP缓存
HTTP缓存应该算是前端开发中最常接触的缓存之一,它又可以细分为强制缓存和协商缓存,二者最大的区别在于判断缓存命中时,浏览器是否需要向服务器端进行询问以协商缓存的相关信息,进而判断是否需要就响应内容进行重新请求,下面让我们来看看HTTP缓存的具体机制及缓存的决策策略。
强制缓存
对于强制缓存而言,如果浏览器判断所请求的目标资源有效命中,则直接从强制缓存中返回请求响应,无须与服务器进行任何通信。
其中与强制缓存相关的两个字段是expires和cache-control,expires是在HTTP1.0协议中声明的用来控制缓存失效日期时间戳的字段,它由服务器端指定后通过响应头告知浏览器,浏览器在接收到带有该字段的响应体后进行缓存。
若之后浏览器再次发起相同的资源请求,便会对比expires与本地当前的时间戳,如果当前请求的本地时间戳小于expires的值,则说明浏览器缓存的响应还未过期,可以直接使用而无须向服务器端再次发起请求。只有当本地时间戳大于expires值,发生缓存过期时,才允许重新向服务器发起请求。
从上述强制缓存的是否过期的判断机制中不难看出,这个方式存在一个很大的漏洞,即对本地时间戳过分依赖,如果客户端本地的时间与服务器端的时间不同步,或者对客户端的时间进行主动修改,那么对于缓存过期的判断可能就无法和预期相符。
为了解决expires判断的局限性,从HTTP1.1协议开始新增了cache-control字段来对expires的功能进行拓展和完善。从上述代码中可见cache-control设置了maxage=31536000的属性值来控制响应资源的有效期,它是一个以秒为单位的时间长度,表示该资源在被请求到后的31536000秒内有效,如此便可避免服务器端和客户端时间戳不同步而造成的问题。
注意:如果Cache-Control的max-age和expires同时存在,则以max-age为准。
Cache-Control的其他参数
- no-cache
设置no-cache并非不适用缓存,而是表示强制进行协商缓存,即对于每次发起的请求都不会再去判断强制缓存是否过期,而是直接与服务器写撒谎给你来验证缓存的有效性,若缓存未过期,则会使用本地缓存。
- no-store
设置no-store则表示禁止使用任何缓存,客户端的每次请求都需要服务器端给予全新的响应。no-cache与no-store是两个互斥的属性值,不能同时设置。
- public
若资源响应头中的cache-control字段设置了public属性值,则表示响应资源既可以被浏览器缓存,又可以被代理服务器缓存。
- private
private则限制了响应资源只能被浏览器缓存,如果没有显示指定则默认值是private。
- max-age
表示服务器端告知客户端浏览器响应资源的过期时长。
- s-maxage
对于大型架构的项目通常会涉及使用各种代理服务器的情况,这就需要考虑缓存在代理服务器上的有效性问题,这边是s-maxage存在的意义,它表示缓存在代理服务器中的过期时长,且仅当设置了public属性值时才是有效的。
由此可见,cache-control能够作为expires的完全替代方案,并且拥有其所不具备的一些缓存控制特性,在项目实践中使用它就足够了,目前expires还存在的唯一理由就是向下兼容。
协商缓存
顾名思义,协商缓存就是在使用本地缓存之前,需要向服务器发起一次GET请求,与之协商当前浏览器保存的本地缓存是否已经过期。通常是采用所请求资源的最近一次的修改时间戳来判断的。
- 实例
假设客户端需要向服务器请求一个manifest.js的JS文件,为了让该资源被再次请求时能够通过协商缓存的机制使用本地缓存,那么首次返回该图片资源的响应头中应包含一个名为last-modified的字段,该字段的属性值为该JS文件最近一次修改的时间戳。
当我们刷新网页时,由于该JS文件使用的是协商缓存,客户端浏览器无法确定本地缓存是否过期,所以需要向服务器发送一次GET请求,进行缓存有效性的协商,此次GET请求的请求头中需要包含一个ifmodified-since字段,其值正是上次响应头中last-modified的字段值。
当服务器收到该请求后便会对比请求资源当前的修改时间戳与if-modified-since字段的值,如果二者相同则说明缓存未过期,可继续使用本地缓存,否则服务器重新返回全新的文件资源。
基于Last-Modified的协商缓存(服务器端代码)
const data = fs.readFileSync('./imgs/CSS.png'); const { mtime } = fs.statSync('./imgs/CSS.png'); const ifModifiedSince = req.headers['if-modified-since']; if (ifModifiedSince === mtime.toUTCString()) { res.statusCode = 304; res.end(); return } res.setHeader('last-modified',mtime.toUTCString()) res.setHeader('Cache-Control','no-cache'); res.end(data); 复制代码
Last-Modified协商缓存流程
客户端第一次请求目标资源的时,服务器返回的响应标头包含last-modified和该资源的最后一次修改的时间戳,以及cache-control:no-cache,当客户端再次请求该资源的时候,会携带一个ifmodifiedsince字段,如果这个字段对应的时间与目标资源的时间戳进行对比,如果没有变化则返回一个304状态码。
需要注意的是:协商缓存判断缓存有效的响应状态码是304,但是如果是强制缓存判断有效的话,响应状态码是200。
last-modified的不足
- last-modified是根据请求资源的最后修改时间戳进行判断的,虽然请求的文件资源进行了编辑,但是内容并没有发生任何变化,时间戳也会更新,从而导致协商缓存时关于有效性的判断验证为失效,需要重新进行完整的资源请求。这无疑会造成网络带宽资源的浪费,以及延长用户获取到目标资源的时间。
- 标识文件资源修改的时间戳单位是秒,如果文件修改的速度非常快,假设在几百毫秒内完成,那么通过时间戳的方式来验证缓存的有效性,是无法识别出该次文件资源的更新的。
其实造成上述两种缺陷的原因相同,就是服务器无法根据资源修改的时间戳识别出真正的更新,进而导致重新发起了请求,该重新请求却使用了缓存的Bug场景。
基于Etag的协商缓存(服务端代码)
为了弥补通过时间戳判断的不足,从HTTP1.1规范开始新增了一个Etag的头信息,即实体标签。 其内容主要是服务器为不同的资源进行哈希计算所生成的一个字符串,该字符串类似于文件指纹,只要文件内容编码存在差异,对应的Etag对文件资源进行更精准的变化感知。
Etag协商缓存的流程
- 首先,服务端将要返回给客户端的数据通过etag模块进行哈希计算生成一个字符串,这个字符串类似于文件指纹。
- 检测客户端的请求标头中的ifNoneMatch字段的值和第一步计算的值是否一致,一致则返回304。
- 如果不一致则返回etag标头和Cache-Control:no-cache。
Etag的不足
不像强制缓存中cache-control可以完全替代expires的功能,在协商缓存中,Etag并非last-modified的替代方案而是一种补充方案,因为依旧存在一些弊端。
- 服务器对于生成文件资源的Etag需要付出额外的计算开销,如果资源的尺寸比较大,数量较多且修改频繁,那么生成的Etag的过程就会影响服务器的性能。
- Etag字段值的生成分为强验证和弱验证,强验证根据资源内容进行生成,能够保证每个字节都相同,弱验证则根据资源的部分属性来生成,生成速度快但无法确保每个字节都相同,并且在服务器集群场景下,也会因为准确不够而降低协商缓存有效性的成功率,所以恰当的方式是根据具体的资源使用场景选择恰当的缓存校验方式。
缓存决策及其注意事项
缓存决策
假设在不考虑客户端缓存容量与服务器算力的理想情况下,我们当然希望客户端浏览器上的缓存触发率尽可能高,留存时间尽可能长,同时还要Etag实现当资源更新时进行高效的重新验证。但实际情况往往是容量与算力都有限,因此就需要制定合适的缓存策略,来利用有限的资源达到最优的性能效果,明确能力的边界,力求在边界内做到最好。
缓存决策树
在面对一个具体的缓存需求时,我们可以参照如下的缓存决策树来逐步确定对一个资源具体的缓存策略。
- 是否使用缓存
- 否:no-store
- 是:
- 是否进行协商缓存
- 是:no-cache
- 否
- 是否会被代理服务器缓存
- 是:public
- 否:private
- 配置强制缓存过期时间
- 配置协商缓存的Etag或last-modified。
CDN缓存
什么是CDN?
CDN全称是内容分发网络,它是构建在现有网络基础上的虚拟智能网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、调度及内容分发等功能模块,使用户在请求所需访问的内容时能够就近获取,以此来降低网络拥塞,提高资源对用户的响应速度。
不使用CDN的通信流程
- 向传统的DNS服务器请求域名解析。
- DNS服务器返回域名对应的服务器IP。
- 根据服务器IP请求服务器内容。
- 服务器返回响应资源。
使用CDN的通信流程
- 客户端向传统的DNS服务器请求域名解析。
- 传统的DNS服务器将域名解析权交给了CNAME指向的专用DNS服务器,所以对用户输入域名的解析最终是在CDN专用的DNS服务器上完成的。
- CDN专用的DNS服务器将CDN负载均衡器的IP发给客户端。
- 浏览器会重新向CDN负载均衡器发起请求,经过对用户IP地址的距离、所请求资源内容的位置等的综合计算,返回给用户确定的缓存服务器IP地址。
- 浏览器最后对缓存服务器进行请求资源。
静态资源适合使用CDN
静态资源指的是不需要网站业务服务器参与计算即可得到的资源,包括第三方库的JavaScript脚本文件、样式表文件以及图片等,这些文件的特点是访问频率高、承载流量大、但更新频次低,且不与业务有太多耦合。
如果是动态资源文件,比如依赖服务器端渲染得到的HTML页面,它需要借助服务器端的数据进行计算才能得到,所以这样的资源不适合存放在CDN缓存服务器上。
CDN的性能优化
下面仅介绍一个CDN优化点:域名设置。
在淘宝的主页上,主站请求的域名为 www.taobao.com ,而静态资源请求CDN服务器的域名有g.alicdn.com和img.alicdn.com两种,这样做的原因有以下两点:
- 避免对静态资源的请求携带不必要的cookie信息。
- 考虑浏览器对同一域名下并发请求的限制。
面试常见问题
问题1:强缓存涉及到哪些请求头?
答:涉及到expires和cache-control两个字段,expires是HTTP1.0协议中的,cache-control是HTTP/1.1协议的。
问题2:为什么现在不用expries用cache control?
答:因为基于expires的强制缓存对本地时间戳过于依赖,如果客户端本地的时间与服务器端的时间不同步,那么对缓存过期的判断可能就会出错。cache-control通过maxage=xxx秒的形式来控制响应资源的有效期,如此可以避免服务器端和客户端时间戳不同步的问题。
问题3:强缓存public private no-store no-catch区别?(Cache-Control有哪些属性?分别表示什么意思?)
public:表示响应资源既可以被客户端缓存也可以被代理服务器缓存。 private:表示响应资源只能被浏览器缓存,如果没有显式指定则默认是private no-store:表示禁止使用任何缓存,每次请求都需要服务器给与全新的响应。 no-cache:表示使用协商缓存。每次请求不再去判断强制缓存是否过期,而是直接向服务器发送请求来验证缓存的有效性。 max-age:表示服务器端告知客户端浏览器响应资源的过期时长。 s-maxage:表示缓存在代理服务器中的过期时长,且仅当设置了public属性值时才是有效的。
问题4:协商缓存的校验是在客户端还是服务器端?协商缓存怎么验证是否命中?
答:服务器端,服务器端会对比文件最后的修改时间和客户端请求携带的时间是否一致,一致则判断命中缓存。协商缓存存在两种形式,一种是基于last-modified,客户端第一次请求目标资源的时候,服务器返回的响应标头中包含last-modified和该资源的最后一次修改的时间戳,以及cache-control:no-cache,当客户端再次请求该资源的时候,会携带一个ifmodifiedsince字段,如果这个字段对应的时间和目标资源的时间戳进行对比,没有变化则返回304状态码。另一种是基于Etag的协商缓存,手下服务端将要返回给客户端的数据通过etag模块进行哈希计算生成一个字符串,这个字符串类似于文件指纹,检测客户端的请求标头中的ifNoneMatch字段的值和第一步计算的值是否一致,一致则返回304,不一致则返回最新的数据以及etag标头和Cache-Control:no-cache。
问题5:协商缓存出于什么原因有Last-Modified,Etag?
答:之所以有last-modified还有etag,是因为这二者均有自己的不足,last-modified是根据请求资源的最后修改时间戳来进行判断的,有可能只是对文件名进行了编辑,但是文件内容并未修改,这样时间戳也会更新,从而导致协商缓存判断失效,请求了已经存在的完整资源,这对网络带宽是一种浪费,也有可能是文件修改的速度是毫秒级别的,但是last-modified的单位是秒,可能无法识别出资源的修改。etag并非last-modified的完全替代方案,只能是一种补充方案,etag存在的问题是,服务器需要对文件资源进行etag计算,需要付出额外的计算开销,如果资源的尺寸比较大,生成Etag的过程可能会影响服务器的性能,所以这也就是为什么协商缓存既有last-modified又有etag的原因了。
问题6:协商缓存和强缓存的区别?
相同点
都是从客户端缓存中读取资源。
不同点
- 如果浏览器命中的是强缓存,则不需要给服务器发请求,而协商缓存最终由服务器来决定是否使用缓存,即客户端与服务器之间存在一次通信。
- 在chrome中命中缓存,返回的状态码是200,而如果是协商缓存,返回的是状态码304。
问题7:expires 和 cache-control 哪个优先级高? 不缓存怎么设置?
答:expires是HTTP/1.0的产物,Cache-Control则是HTTP/1.1的产物,二者如果同时存在的话,Cache-Control优先级比Expires高。不缓存则是通过Cache-Control:no-store设置。
问题8:LastModified 对应有个请求头是什么?
last-modified-since.
问题9:缓存的优先级顺序?
答:Cache-Control > Expires > Etag > Last-Modified。