缓存的重要性
- 缓存一般针对不经常进行更新的静态资源
- 缓存的原理是在首次请求之后保存一份请求资源的响应副本,当用户再次发起相同的请求后,如果判断缓存命中则拦截请求,将之前存储的响应副本返回给用户,从而避免重新向服务器发起资源请求
- 缓存的技术种类有很多,例如代理缓存、浏览器缓存、网关缓存、负载均衡及内容分发网络等,它们大致可分为两类:共享缓存和私有缓存,共享缓存指缓存的内容可以被多个用户使用,如公司架设的web代理,私有缓存是指只能单独被用户使用的缓存,如浏览器缓存
- http缓存是前端开发中最常接触的缓存机制之一,它可细分为强制缓存和协商缓存,两者最大的区别在于缓存命中时,浏览器是否需要向服务器端进行询问以协商缓存的相关信息,进而判断是否需要就响应内容进行重新请求
强制缓存
对于强制缓存而言如果浏览器判断所请求的目标资源有效命中,则可直接从强制缓存中返回请求响应,无需与服务器进行任何通信
在介绍强制缓存命中之前我们首先看一段响应头信息
access-control-allow-origin: *
age: 734978
content-length: 40830
content-type: image/jpeg
cache-control: max-age=31536000
expires: Web, 14 Fed 2021 12:23:42 GMT
- expires: http1.0中用来控制缓存失效日期的时间戳字段,它由服务端指定后通过响应头告知浏览器,浏览器接收到带有该字段的响应体后进行缓存,若之后的浏览器再次发起相同资源请求,便会对比expires与本地当前时间戳,只有当本地时间大于expires时间之后才会允许重新向服务器再次发起请求,否则一直使用的都是缓存,但是这种方式有很大漏洞,就是当本地时间不准确时候,判断也自然不准确。
- cache-control:为了解决expires的局限性,从http 1.1开始新增了此字段对expires的功能进行扩展和完善,从上述代码中可看到cache-control设置了 max-age=31536000的属性来控制响应资源的有效期,它是一个以秒为单位的时间长度,表示资源在请求到之后的31536000秒内有效
- cache-control的参数:
- no-cache:强制进行协商缓存
- no-store: 禁止使用任何缓存策略,客户端每次请求都需要服务端给予最新的响应,与no-cache互斥,不能同时设置
- public: 表示响应资源既可以被浏览器缓存,也可被代理服务器缓存
- private: 限制了响应资源只能被浏览器缓存,如果未显示指定public和private默认值为private
- max-age: 表示服务器端告知客服端浏览器响应资源的过期时常
- s-maxage: 表示缓存在代理服务器上面的时效,只有当设置了public的时候才能使用
协商缓存
- 协商缓存就是在使用本地缓存之前需要向服务器端发起一次GET请求,与之协商当前浏览器保存的本地缓存是否已经过期
协商缓存方法一
- 在后台设置响应头last-modified 为文件的最新一次修改时间,且必须在响应头上添加cache-control: no-cache,否则无效
- 当前端接收到带有last-modified的响应的时候,再次发送请求会在请求头上自动带上if-modified-since的字段,这个字段的值就是服务端响应头中的last-modified的值。然后会有一个去服务端跟最新的资源更新时间也就是last-modified字段做对比,如果相同,服务端应该返回一个304的响应且不返回该资源,如果不相同说明资源更新了,返回200响应并返回最新资源
- 此方法存在的问题
- 如果文件资源只是进行了编辑,但内容无变化,但是最新一次修改的时间戳也会更新,会造成不必要的网络请求资源的浪费
- 最新一次修改的时间戳单位是秒,如果文件修改的很快在几百毫秒之内修改完成了,也是无法识别出来是否更新的,最新一次修改时间无变化
协商缓存方法二
- 为了弥补时间戳方式的不足,在http1.1规范中新增了一个ETag的头信息,即实体标签,其内容主要是服务器为不同资源进行哈希运算所生成的一个字符串,该字符串类似文件指纹,只要文件内容编码存在差异,对应的ETag标签值就会不同,因此可以使用ETag对文件资源进行更精准的变化感知
- 步骤如下
- 服务端根据对应的资源生成一个对应的唯一值,然后当客户端首次请求这个资源的时候,把这个资源对应的唯一值放在响应头的ETag字段中返回,注意,同时必须在响应头上添加cache-control: no-cache,否则无效
- 客户端首次接收到资源之后,如果后续再请求相同资源,客户端请求头中会自动携带if-None-Match的字段,此字段对应第一次请求资源响应头中的ETag字段
- 服务端判断当前资源的if-None-Match字段是否与ETag字段相同,如果相同返回304,如果不同返回200和最新的资源
- 此方法存在的一些问题
- 服务器端对生成文件资源的ETag需要付出额外的计算开销,如果资源尺寸较大且修改较为频繁,那么生成ETag的过程会影响服务器性能
- ETag分为强验证和弱验证,强验证根据资源内容生成,保证每个字节都相同,弱验证根据资源部分属性值生成,但无法保证每个字节都相同,且在服务器集群的场景下,也会因为不够准确而降低协商缓存有效性验证的成功率,所以应该根据具体的资源使用场景来选择恰当的缓存校验方式
缓存决策及注意事项
-
缓存决策树 -
缓存决策示例子 在使用缓存技术优化性能体验的过程中,有一个问题是不可逾越的:我们既希望缓存能在客户端尽可能久的保存,又希望它能在资源发生修改时进行及时更新。 这是两个互斥的优化诉求,使用强制缓存并定义足够长的过期时间就能让缓存在客户端长期驻留,但由于强制缓存的优先级高于协商缓存,所以很难进行及时更新;若使用协商缓存,虽然能够保证及时更新,但频繁与服务器进行协商验证的响应速度肯定不及使用强制缓存快。那么如何兼顾二者的优势呢? 我们可以将一个网站所需要的资源按照不同类型去拆解,为不同类型的资源制定相应的缓存策略,以下面的HTML文件资源为例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTTP 缓存策略</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<img src="photo.jpg" alt="poto">
<script src="script.js"></script>
</body>
</html>
-
该 HTML 文件中包含了一个 JavaScript 文件 script.js、一个样式表文件 style.css 和一个图片文件 photo.jpg,若要展示出该 HTML 中的内容就需要加载出其包含的所有外链文件。据此我们可针对它们进行如下设置。 -
首先HTMl在这里属于包含其他文件的主文件,为了保证当其中内容发生改变时候能及时的更新,应当将其设置为协商缓存。即为cache-control 字段添加为 no-cache 属性值,其次是图片文件,因为网站对图片的修改基本都是更换修改,同时考虑到图片文件的数量以及大小可能对客户端缓存造成不小的开销,所以可以采用强制缓存,且时间不宜过长,故可以设置cache-contorl 字段值为 max-age= 86400 (24小时) -
接下来需要考虑的是样式表文件 style.css,由于其属于文本文件,可能存在内容的不定期修改,又想使用强制缓存来提高重用效率,故可以考虑在样式表文件的命名中增加文件指纹或版本号(比如添加文件指纹后的样式表文件名变为了 style.51ab84.css ),这样当发生文件修改后,不同的文件便会有不同的文件指纹,即需要请求的文件 URL 不同了,因此必然会发生对资源的重新请求。同时考虑到网络中浏览器与 CDN 等中间代理的缓存,其过期时间可适当延长到一年,即 cache-control:max-age=3156000 -
最后是 JavaScript 脚本文件,其可类似于样式表文件的设置,采取文件指纹和较长的过期时间,如果 JavaScript 中包含了用户的私人信息而不想让中间代理缓存,则可为 cache-control 添加 private -
从这个缓存策略的示例中我们可以看出,对不同资源进行组合使用强制缓存、协商缓存及文件指纹或版本号,可以做到一举多得:及时修改更新、较长缓存过期时间及控制所能进行缓存的位置。
缓存设置的注意事项
在前面的内容中虽然给出了一种制定缓存决策的思路与示例,但需要明白的一点是:不存在适用于所有场景下的最佳缓存策略。凡是恰当的缓存策略都需要根据具体场景下的请求资源类型、数据更新要求及网络通信模式等多方面因素考量后制定出来,所以下面列举一些缓存决策时的注意事项,来作为决策思路的补充。
-
拆分源码,分包加载 对大型的前端应用迭代开发来说,其代码量通常很大,如果发生修改的部分集中在几个重要模块中,那么进行全量的代码更新显然会比较冗余,因此我们可以考虑在代码构建过程中,按照模块拆分将其打包成多个单独的文件。这样在每次修改后的更新提取时,仅需拉取发生修改的模块代码包,从而大大降低了需要下载的内容大小。 -
预估资源的缓存实效 根据不同资源的不同需求特点,规划相应的缓存更新时效,为强制缓存指定合适的 max-age 取值,为协商缓存提供验证更新的 Etag 实体标签 -
控制中间代理的缓存 凡是涉及用户隐私信息的尽量避免中间代理的缓存,如果对所有用户响应相同的资源,则可以考虑中间代理也进行缓存。 -
避免网站的冗余 缓存是根据请求资源的 URL 进行的,不同的资源会有不同的 URL ,所以尽量不要将相同的资源设置为不同的 URL。 -
规划缓存的层次结构 参考缓存决策中介绍的示例,不仅是请求的资源类型,文件资源的层次结构也会对制定缓存策略有一定影响,我们应当综合考虑。
缓存是限定域名的
- 根域下的缓存是共享的。比如 a.com、foo.a.com、bar.a.com 的根域都是 a.com,他们是共享缓存;
- 同理,域名不同的缓存不共享。比如 a.com、b.com、c.com,他们之间即使加载相同资源也仅在该域名下有效,不共享。
CDN缓存
- CDN即内容分发网络,它是构建在现有网络基础上的虚拟智能网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、调度及内容分发等功能模块,使用户在请求所需访问的内容时能够就近获取,以此来降低网络堵塞,提高资源对用户的响应速度。
- 如果未使用 CDN 网络进行缓存加速,那么通过浏览器访问网站获取资源的大致过程如图所示
- 当用户在浏览器中输入所要访问的域名时,若本机无法完成域名解析工作,则会转向 DNS 服务器请求对该域名的解析
- DNS 服务器解析完成返回给浏览器该域名所对应的 IP 地址
- 浏览器向该 IP 地址指向服务器发起资源请求
- 最好服务器响应用户请求将资源返回给浏览器
-
如果使用 CDN 网络,则资源获取的大致过程是这样。 -
由于 DNS 服务器将对 CDN 的域名解析交给 CNAME 指向的专用 DNS 服务器,所以对用户输入域名的解析最终是在 CDN 专用的 DNS 服务器上完成的。 解析出的结果 IP 地址并非确定的 CDN 缓存服务器地址,而是 CDN 的负载均衡的地址。 浏览器会重新向该负载均衡服务器发起请求,经过对用户 IP 地址的距离,所请求资源内容的位置以及各个服务器负载状况的综合计算,返回给用户确定的缓存服务器级地址。 对目标缓存服务器请求所以资源的过程
针对静态资源
-
CDN 网络能够缓存网站资源来提升首次请求的响应速度,但并非能适用于网站所有资源类型,它往往仅被用来存放网站的静态资源文件。所谓静态资源,就是指不需要网站业务服务器参与计算即可得到的资源,包括第三方库的 JavaScript 脚本文件、样式表文件及图片等,这些文件的特点是访问频率高、承载流量大,但更新修改频次低,且不与业务有太多耦合。 -
如果是动态资源文件,比如依赖服务器端渲染得到的 HTML 页面,它需要借助服务器端的数据进行计算才能得到,所以它就不适合放在 CDN 缓存服务器上。 核心功能
应用场景
-
以淘宝为例 如下图 此时打开 Chrome 开发者工具的 Network 选项卡,来查看网站为渲染出该效果此时打开 Chrome 开发者工具的 Network 选项卡,来查看网站为渲染出该效果都请求了哪些资源,我们很容易发现除了从业务服务器返回的一个未完全加载资源的 HTML 文件,还包括了许多图片、JavaScript 文件及样式表文件,具体内容如图所示。 接着我们进一步去查看静态资源所请求的URL,并列举几种不同类型的资源文件如下: 从上述资源文件的请求域名中我们可以发现,这些文件都是从 CDN 网络上获取的,JavaScript 和样式表这样的文本文件与图片文件使用的是不同的 CDN 域名,而且 CDN 域名与主站域名也完全不同,这样的设计也是出于对性能的考虑,下面来分析具体的优化原理。 优化实践 -
关于 CDN 的性能优化,如何能将其效果发挥到最大程度?其中包括了许多可实践的方面,比如 CDN 服务器本身的性能优化、动态资源静态边缘化、域名合并优化和多级缓存的架构优化等,这些可能需要前端工程师与后端工程师一起配合,根据具体场景进行思考和解决,这里仅介绍一个与前端关系密切的 CDN 优化点:域名设置。 我们以上面示例来说明,主站请求的域名是www。taobao.com ,而静态资源请求的 CDN 服务器的域名有 g.alicdn.com 和 img.alicdn.com 两种,他买是有一设计与主站域名不同的,这样原因主要有两点:第一点是避免对静态资源的请求携带不必要的 Cookie 信息,第二点是考虑浏览器对统一域名下并发请求的限制。
- 首先对第一点来说,Cookie 的访问遵循同源策略,并且同一域名下的所有请求都会携带全部 Cookie 信息。
虽然 Cookie 的存储空间就算存满也并不是很大,但如果将所有资源请求都放在主站域名下,那么所产生的效果对于任何一个图片、JavaScript 脚本及样式表等静态资源文件的请求,都会携带完整的 Cookie 信息,若这些完全没有必要的开销积少成多,那么它们所产生的流量浪费就会很大,所以将 CDN 服务器的域名和主站域名进行区分是非常有价值的实践。 - 其次是第二点,因为浏览器对于同域名下的并发请求存在限制,通常 Chrome 的并发限制数是 6,其他浏览器可能多少会有所差异。这种限制也同时为我们提供了一种解决方案:通过增加类似域名的方式来提高并发请求数,比如对多个图片文件进行并发请求的场景,可以通过扩展如下类似域名的方式来规避限制:
虽然这种方式对于多并发限制是有效的,但是缓存命中是要根据整个 URL 进行匹配的,如果并发请求了相同的资源却又使用了不同的域名,那么图片之前的缓存就无法重用,也降低了缓存的命中,对于这种情况我们应该考虑进行恰当的域名合并优化。
|