HTTP缓存策略只是为了解决客户端和服务端信息不对称的问题而存在的,客户端为了加快速度会缓存部分资源,但是下次请求时,客户端不知道这个资源有没有更新,服务端也不知道客户端缓存的是哪个版本,不知道该不该再返回资源,其实就是一个信息同步的问题。
缓存分为2种,一种是强缓存,另一种是协商缓存。主要作用是可以加快资源获取速度,提升用户体验,减少网络传输,缓解服务端的压力。
强缓存
不用跟服务器协商直接用本地缓存的叫做强制缓存。
强缓存,引用 https://segmentfault.com/a/1190000038562294 中的例子。
Expires
就是我服务器端,直接在你第一次请求服务器资源的时候,我就给你加上一个字段,告诉你想请求的时候,先自己看看时间,对对表(看看服务器给的Expires),你有没有到和我(服务器)接头的时间。没有到就不接头,到了时间再接头。
技术话: Expires的值是一个HTTP日期, 在浏览器发起请求时,会根据系统时间和Expires的值进行比较,如果系统时间超过了Expires的值,缓存失效。由于和系统时间进行比较,所以当系统时间和服务器时间不一致的时候,会有缓存有效期不准的问题。
例: Expires: Wed, 21 Oct 2000 07:28:00 GMT
Cache-Control
Cache-Control是HTTP/1.1中新增的属性,在请求头和响应头中都可以使用,常用的属性值有:
● max-age:单位是秒,缓存时间计算的方式是距离发起的时间的秒数,超过间隔的秒数缓存失效
● no-cache:不使用强缓存,需要与服务器验证缓存是否新鲜
● no-store:禁止使用缓存(包括协商缓存),每次都向服务器请求最新的资源
● private:专用于个人的缓存,中间代理、CDN 等不能缓存此响应
● public:响应可以被中间代理、CDN 等缓存
private和public是用来明确相应资源是否可以被代理服务器进行缓存
● must-revalidate:在缓存过期前可以使用,过期后必须向服务器验证
- max-age
设置缓存存储的最大周期,超过这个时间缓存会被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间。假设 在第一次请求资源的时候,设置
‘Cache-Control’: ‘max-age=10’;
那么下次请求如果在10秒内,就直接用浏览器缓存,如果在10秒外,就直接进行请求。 - no-cache
不像字面上的意思不使用缓存(强制不使用缓存的是no-store), 它的意思是,强制使用协商缓存。 - private
限制了响应资源只能被浏览器缓存,若未显式指定则默认值为private - public
如果设置了这个属性, 那么说明响应资源可以被浏览器缓存,又可以被代理服务器缓存。
对于应用程序中不会改变的文件,通常可以在发送响应头前添加积极缓存。这包括例如由应用程序提供的静态文件,例如图像,CSS文件和Javascript文件。
掘金我看了一下,好像只用到了max-age, 上面的截图是segmentfault的,大家有兴趣可以去看下。
注: 如果在Cache-Control响应头设置了max-age或者s-maxage指令,那么Expires头会被忽略。Cache-Control是expires的弯曲替代方案。
协商缓存
需要发请求去问服务器有没有更新,如果有更新就重新下载资源,如果没有更新就返回304(表示没有变化),直接使用浏览器缓存。
协商缓存
Last-Modified 和 If-Modified-Since
Last-Modified和If-Modified-Since是配套使用的,Last-Modified放的是资源的最后修改时间,在第一次访问服务器资源的时候返回给浏览器。
例: Last-Modified: Wed, 21 Oct 2000 07:28:00 GMT
在再次发送请求时,将这个字段,存放在请求头的If-Modified-Since里。服务器接收到请求后会使用这个字段的数据和文件本身的这个最后修改时间进行比较, 如果相同,那么返回304,告诉浏览器直接使用缓存。如果不同,那么就返回200和重新请求的数据。
但是last-modified也有一些不足。
● 首先它只是根据资源最后的修改时间戳进行判断的,虽然请求的文件资源进行了编辑,但内容没有发生任何变化,但是时间戳也会更新,从而导致协商缓存时关于有效性的判断验证为失效,需要重新进行完整的资源请求,这无疑会造成网络带宽资源的浪费,从而延长用户获取到目标资源的时间。
● 其次标记文件资源修改的时间戳单位是秒,如果文件修改的速度非常快,假设在几百毫秒内完成,那么上述通过时间戳方式来验证缓存的有效性,是无法识别出该次文件资源的更新的。
造成上述两种缺陷的原因相同,就是服务器无法仅依据资源修改的时间戳来标识出真正的更新,进而导致重新发起了请求,该重新请求却使用了缓存的Bug场景。
所以就有了下面的基于ETag协商缓存。
ETag和If-None-Match
ETag是URL的Entity Tag(实体标签),就是一个URL资源的标识符,类似于文件的md5,计算方式也类似(使用了哈希算法),当服务器第一次返回资源的时候,会在文件的响应头(response)中添加Etag。
例如: ETag: “33a64df551425fcc55e4d42a148795d9f25f89d4”
客户端拿到后会将这个ETag和返回值一起存下来,等下次请求时,会在发送的GET请求中添加一个请求头If-None-Match字段, 当有这个请求头的时候,服务器端就会去判断携带来的If-None-Match和文件的Etag是否相同, 如果相同就返回304(Not Modified)不返回内容,如果不相同就返回200和最新的内容。
例: If-None-Match: “33a64df551425fcc55e4d42a148795d9f25f89d4”
节约服务器文件读取的时间,减轻服务器响应压力。
ETag是一个不透明的标识符,由Web服务器根据URL上的资源的特定版本而指定。如果那个URL上的资源内容改变,一个新的不一样的ETag就会被分配。用这种方法使用ETag即类似于指纹,并且他们能够被快速比较,以确定两个版本的资源是否相同。
附加知识: If-Match
引用: If-Match通常用于post或者put请求中,语义为如果匹配才提交,比如你在编辑一个商品,其他人也可能同时在编辑。当你提交编辑时,其他人可能已经先于你提交了,这时候服务端的ETag就已经变了,If-Match就不成立了,这时候服务端会给你返回412错误,也就是Precondition Failed,前提条件失败。如果If-Match成立,就正常返回200。
感觉理解起来就是锁。
ETag的不足
不像强制缓存中cache-control可以完全替代expires的功能,在协商缓存中,ETag并非last-modified的替代方案二是一种补充方案。
服务器对于生成文件资源的ETag需要付出额外的计算开销,如果资源的尺寸较大,数量较多且修改比较频繁,那么生成ETag的过程就会影响服务器的性能。
缓存优化
我们既希望缓存能在客户端尽可能久的保存,又希望它能在资源发生修改的时候进行及时更新。
我们可以将一个网站所需要的资源按照不同类型去拆解,为不同类型的资源制定相应的缓存策略。
以一个HTML文件举例:其中包含了css文件,js文件,图片。
HTML在这里是属于包含其他文件的主文件,为保证当其内容发生修改时能及时更新,应当将其设置为协商缓存。其次是图片文件,因为网站对图片的修改基本都是更换修改,同时考虑到图片文件的数量以及大小可能对客户端缓存空间造成不小的开销,所以可采用强制缓存且过期时间不宜过长,故可设置cache-control字段值为max-age=86400。接下来需要考虑的是样式表style.css,由于其属于文本文件,可能存在内容的不定期修改,又想使用强制缓存来提高重用效率,故可以考虑在样式表文件的命名中增加文件指纹或版本号(比如添加文件指纹后的样式表文件名变成了style.xxxxxx.css(现在的工程化工具都有这个功能),webpack, vite),这样当发生文件修改后,不同的文件便会有不同的文件指纹,即需要请求的文件URL不同了,因此必然会发生对资源的重新请求。同时考虑到网络中浏览器与CDN等中间代理的缓存,其过期时间可适当延长到一年,即cache-control:max-age=31536000.
最后是Javascript脚本文件,其可类似于样式表文件的设置,如果Javascript中包含了用户的私人信息而不想让中间代理缓存,则可为Cache-Control添加private属性值。
另外的一些思路
- 拆分源码,分包加载
对大型的前端应用迭代开发来说,其代码量通常很打,如果发生修改的部分集中在几个重要的模块中,那么进行全量的代码更新显然会比较冗余,因此我们可以考虑在代码构建过程中,按照模块拆分将其打包成多个单独的文件。这样在每次修改后的更新提取是,仅需拉取发生修改的模块代码包,从而大大降低了需要下载的内容大小。 - 预估资源的缓存时效
根据不同资源的不同需求特点,规划相应的缓存更新时效,为强制缓存指定合适的max-age取值,为协商缓存提供验证更新的ETag实体标签。 - 控制中间代理的缓存
凡是会涉及用户隐私信息的尽量避免中间代理的缓存,如果对所有用户响应的资源,则可以考虑让中间代理也进行缓存。 - 避免网站的冗余
缓存是根据请求资源的URL进行的,不同的资源会有不同的URL,所以尽量不要将相同的资源设置为不同的URL. - 规划缓存的层次结构
参考上面文件分层。
参考
- https://www.bilibili.com/video/BV1Jr4y1v7Nc?p=6&spm_id_from=pageDriver&vd_source=ff0a7b8d991bc5fabca1f60088c81d77
- https://segmentfault.com/a/1190000038562294
- https://www.infoq.cn/article/aiwqlgtlk2eft5yi7doy
- mdn