缓存概念
缓存的应用非常广泛,在前端更是有着举足轻重的地位,是解决性能问题最常用的手段之一。
缓存在我们的项目中可谓是无处不在,小到一个函数的执行结果,大到图片资源、服务器请求的数据可以进行缓存。
缓存之所以这么重要,是因为它能带来非常多的好处, 如:
- 加快网页加载和呈现速度。
- 减少了不必要的的请求,因而节省网络流量和带宽同时也减少服务器的负担。
前端的缓存大致分为两类:HTTP 缓存和浏览器缓存。
浏览器缓存又包括默认缓存和本地存储,如Cookie、WebStorage、WebSql、indexDB、Application Cache、PWA等。
HTTP 缓存
本文要详细说的是 HTTP 缓存。
HTTP 缓存指的是: 当客户端向服务器请求资源时,会先抵达浏览器缓存,如果浏览器有“要请求资源”的副本,就可以直接从浏览器缓存中提取而不是从原始服务器中提取这个资源。
常见的http缓存只能缓存get请求响应的资源,对于其他类型的响应则无能为力,所以后续说的请求缓存都是指GET请求。
HTTP 缓存的分类
资源缓存分为两类: from disk cache
(磁盘)和from memory cache
(内存) 。
from disk cache
和from memory cache
是如何搭配工作的呢 ?
当用户首次访问网页时,资源文件被缓存在内存中,同时也会在本地磁盘中保留一份副本。
当用户刷新页面,如果缓存的资源没有过期,那么直接从内存中读取并加载。当用户关闭页面后,当前页面缓存在内存中的资源被清空。
当用户再一次访问页面时,如果资源文件的缓存没有过期,那么将从本地磁盘进行加载并再次缓存到内存之中。
需要注意的是虽然资源被同时存储在内存和磁盘缓存中,但是绝大部分浏览器都会 优先使用内存缓存, 因为读取内存缓存的速度更快。存储再磁盘缓存的目的是防止浏览器崩溃或者主动退出,因为磁盘缓存是持久的。
当然缓存也存在有效期,我们称之为缓存的失效策略,如果以此分类,又分为:
- 强缓存
- 协商缓存
理解缓存的策略是学习缓存的核心,下面分别介绍两种策略。
强缓存
强缓存是指客户端在第一次请求后,将得到的数据进行缓存,有效时间内不会再去请求服务器,而是直接使用缓存数据。所有的数据都不是永恒不变的,到底什么时候再次重新请求呢?那么这个过程,就涉及到一个缓存有效期的判断。HTTP 1.0 和 HTTP 1.1 在判断逻辑上是有差异的。
HTTP 1.0 版本:规定响应头字段 Expires,它对应一个未来的时间戳。客户端第一次请求之后,服务端下发 Expires 响应头字段,当客户端再次需要请求时,先会对比当前时间和 Expires 头中设置的时间。如果当前时间早于 Expires 时间,那么直接使用缓存数据。反之,需要再次发送请求,更新数据。
响应头如:
// Response Headers
Expires: Wed, 10 June 2020 15: 32: 12 GMT
上述 Expires 信息告诉浏览器:在2020年6月10号15点32分12秒之前,可以直接使用缓存不用请求数据。
注意: Expires 响应头的方式对服务器端和客户端有时间的要求, 时间必须一致, 并且Expires 响应头对格式要求非常严格。
HTTP 1.1 版本:服务端使用更加强大的 Cache-control 响应头,它具有多个配置值:
- private:表示私有缓存,不能被共有缓存代理服务器缓存,不能在用户间共享,可被用户的浏览器缓存。
- public:表示共有缓存,可被代理服务器缓存,比如 CDN,允许多用户间共享。
- max-age:值以秒为单位,表示缓存的内容会在该值后过期。
- no-cache:需要使用协商缓存,协商缓存的内容我们后面介绍。注意这个字段并不表示不使用缓存。
- no-store:所有内容都不会被缓存。
- must-revalidate:告诉浏览器,你这必须再次验证检查信息是否过期, 返回的代号就不是 200 而是 304 了。
我们看这样的 Cache-control 设置:
// Response Headers
Cache-Control: private, max-age=3600, must-revalidate
它表示:该资源只能被浏览器缓存。max-age=3600 说明该缓存资源3600秒后过期。
注意:HTTP 规定,如果 Cache-control 的 max-age 和 Expires 同时出现,那么 max-age 的优先级更高,他会默认覆盖掉 Expires 。
协商缓存
强缓存判断的实质上是缓存资源是否超出某个时间或者某个时间段。但问题是很多情况是超出了这个时间或时间段,但是资源并没有更新。但是还是要发起一次请求去请求没有改变的数据或资源。
从优化的角度来说,有些情况下我们真正应该关心的是服务器端文件是否已经发生了变化,如果改变了我们再去请求而不是一贯的遵循时间规定。此时我们需要用到协商缓存策略。
那么问题来了,如何知道服务器端文件是否已经发生改变了呢?
强缓存关于是否使用缓存完全是由浏览器决定的,想让浏览器自己知道服务器端文件是否已经发生了变化是不可能的。
那么使用协商缓存时否是使用缓存的决定权必然要交给服务端,因此协商缓存不可避免的要发起一次网络请求。
协商缓存过程:在浏览器端,当对某个资源的请求不采取强缓存策略时(第一次请求服务器返回的响应头没有 Cache-Control 和 Expires 或者 Cache-Control 和 Expires 过期再或者它的属性设置为 no-cache),浏览器就会发一个请求到服务器,验证是否采取协商缓存策略,如果采取协商缓存,请求响应返回的 HTTP 状态就为 304。
服务端如何判断资源有没有过期?
服务端掌握着最新的资源,那么为了做对比,它需要知道客户端的资源信息。根据 HTTP 协议,通过【ETag、If-None-Match】和【Last-Modified,If-Modified-Since】这两对 header 来作出决定。
【Last-Modified,If-Modified-Since】 这一对 header 主导的协商缓存过程:
- 浏览器第一次请求资源,服务端在返回资源的响应头中加入 Last-Modified 字段,这个字段表示这个资源在服务器上的最近修改时间如下:
Last-Modified: June, 10 Jan 2020 16:41:13 GMT
- 浏览器收到响应,并记录 Last-Modified 这个响应头的值为 T。
- 当浏览器再次向服务端请求该资源时,请求头加上 If-Modified-Since 的 header,这个 If-Modified-Since的值正是上一次请求该资源时,后端返回的 Last-Modified 响应头值 T。
- 服务端再次收到请求,根据请求头 If-Modified-Since 的值 T,判断相关资源是否在 T 时间后有变化。如果没有变化则返回 304 , Not Modified,且并不返回资源内容,浏览器使用资源缓存值;如果有变化,则正常返回资源内容,且更新 Last-Modified 响应头内容
但是这种协商缓存的策略依然存在强缓存策略中的问题,就是如果客户端时间不准就会导致时间验证出现问题,并且一些文件也许会周期性的更改,但是他的内容并不改变,仅仅改变的修改时间。并且 Last-Modified 标注的最后修改只能精确到秒,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间。这些情况下使用 Last-Modified 就不是很适合了。
为了解决这个问题,就有了 【ETag、If-None-Match】这一对 header 头来进行协商缓存的判断。
【ETag、If-None-Match】这一对 header 主导的协商缓存过程:
- 浏览器第一次请求资源,服务端在返回资源的响应头中加入 Etag ,Etag 能够弥补 Last-Modified 的问题,因为 Etag 的生成过程类似文件 hash 值,Etag 是一个字符串,不同文件内容对应不同的 Etag 值:
response Headers: ETag: "55D7B35D71DA3051F63AB95F9608F821
- 浏览器收到响应,记录 Etag 这个响应头的值为 E。
- 浏览器再次跟服务器请求这个资源时,在请求头上加上 If-None-Match,值为 E。
- 服务端再次收到请求,获得请求头 If-None-Match 的值 E,根据资源生成一个新的 ETag,对比 E 和新的 Etag:如果两值相同,则说明资源没有变化,返回 304 Not Modified;如果两值不同,就正常返回资源内容,同时携带着新的 ETag 响应头。
- 浏览器收到 304 的响应后,就会从缓存中加载资源。
Etag 的生成策略,实际上并没有强制的规范,这就取决于各大厂商或平台的自主实现方式了。
另外需要注意的细节是:
- Etag 优先级比 Last-Modified 高,如果他们组合出现在请求头当中,我们会优先采用更加完善的 Etag 策略。
- 对于使用服务器集群来处理请求的网站来说,相同的资源,在两台服务器产生的 Etag 值是不是相同的,此种情况服务器需要更大的开销来匹配 Etag ,所以当使用集群服务器时谨慎考虑是否使用 Etag。
缓存行为
当我们在浏览器进行操作的时候何时会触发缓存行为呢?
由于浏览器引擎的差异,缓存行为的表现可能会略有差异,以 Chrome 为例:
1. 当在浏览器中进行地址栏回车、页面链接跳转、新开窗口、前进后退操作时,强缓存和协商缓存都有效,根据优先级进行匹配。
2. 当进行 F5 刷新时跳过强缓存,但是协商缓存仍然有效。
3. 当使用 Ctrl + F5 进行强制刷新时,强缓存和协商缓存均失效。
如何使用HTTP缓存 ?
一般需要缓存的资源有html页面和其他静态资源:
html页面缓存的设置主要是在<head>标签中嵌入<meta>标签,这种方式只对页面有效,对页面上的资源无效
1.1. html页面禁用缓存的设置如下:
主流浏览器可识别的标签
<meta http-equiv="cache-control" content="no-cache">
1.2. html设置缓存如下:
主流浏览器识别的标签
<meta http-equiv="Cache-Control" content="max-age=7200" />
仅有IE浏览器识别的标签
<meta http-equiv="Expires" content="Mon, 20 Aug 2018 23:00:00 GMT" />
静态资源的缓存一般是在web服务器上配置的。
缓存策略优先级
各种缓存策略在优先级上:Cache-Control > Expires > ETag > Last-Modified 。
总结
对于缓存的使用我们可以大体的遵循以下两条原则:
- 强制缓存优先级最高,并且无论资源是否改动在缓存有效期内浏览器都不会发送请求,因此强缓存的使用适用于大型且不易修改的的资源文件,例如第三方 CSS、JS 文件或图片资源。
- 协商缓存灵活性高,适用于数据的缓存,并且 Etag 的灵活性更高。