一文读懂前端缓存
什么是前端缓存?
基本的网络请求就是三个步骤: 请求,处理,响应。
前端缓存主要在“请求”和“响应”中进行。 在请求步骤中, 浏览器也可以通过存储结果的方式直接使用资源,直接省去了发送请求;而相应的步骤需要浏览器和服务器共同配合,通过减少响应内容来缩短传输时间。
本文主要包括
- 按存储位置分类(memory cache, disk cache, Service Worker等)
- 按失效策略分类(Cache-Control, ETag等)
按缓存位置分类
memory cache 是内存中的缓存,(与之相对应的 disk cache是硬盘上的缓存)。按照操作系统的常理:先读缓存,再读硬盘。几乎所有的网络请求资源都会被浏览器自动加入到memory cache中。但是因为数量很大并且浏览器占用的内存不能无限放大这两个因素,memory cache只能是个短期存储,常规情况下,浏览器的Tab关闭后memory cache则失效,如果一个页面的缓存占用了超级多的内存,可能在Tab没关闭之前, 前面的缓存就已经失效了
上面提到的“几乎所有请求资源都能进入 memory cache”, 这里细分一下主要有两块(preloader和preload)
1. preloader,做一个简单的介绍,在浏览器打开网页的过程中,会先请求HTML然后解析。之后如果浏览器发现了js,css等需要解析和执行的资源时,它会使用CPU资源对他们进行解析和执行。在 2007年以前, “请求js/css-解析执行 - 请求下一个js/css-解析执行 - 请求下一个js/css-解析执行”这样的操作模式在每次打开页面之前进行着,很明显在解析执行的时候,网络请求是空闲的, 我们能不能一遍解析执行js/css一边请求下一个或者下一批资源呢?这就是preloader要做的事情,preloader没有官方标准,所以每个浏览器的处理略有区别。例如有些浏览器还会下载css中的 @import内容或者<video>的poster.而这些被preloader请求过来的资源就会被放入memory cache中,供之后的解析执行操作使用。
2. preload,虽然和preloader差了两个字母,实际上这个大家应该更加熟悉一点, 例如<link rel = "preload">,这些显示指定的预加载资源,也会被放入memory cache中。
mechery cache机制保证了一个页面如果有两个相同的请求(例如两个相同src的相同的<img>,两个相同的href的<link>)都实际只会被请求做多一次,避免浪费。
不过,在匹配缓存时,除了匹配完全相同的URL之外,还会比对他们的类型CORS中的域名规则等, 因此一个作为脚本(script)类型被缓存的资源是不能在图片类型的请求中的,即便他们src相等。
再从mechery cache获取缓存内容时, 浏览器会自动忽略例如 max-age = 0, no-cache等头部配置。例如页面上存在几个相同的 src 的图片,即便他们可能被设置为不缓存,但依然会从machery cache中读取,这是因为memory cache只是短期使用,大部分情况生命周期只有一次浏览而已。而 max-age=0 在语义上普遍被解读为“不要在下次浏览使用”,两者是不冲突。但如果真心不想让一个资源进入缓存,需要使用 no-store. 存在这个这个头部配置的话,即便是 mechery cache也不会存储。
disk cache 也叫HTTP cache.,顾名思义是存储在硬盘上的缓存, 因此它是持久存储的,是实际存在于文件系统中,允许相同的资源在跨会话, 甚至跨站点的情况下使用,例如两个站点都是用一张照片,disk cache会严格根据HTTP头信息中的各类字段来判定那些资源可以缓存,那些资源不可以缓存,那些资源是仍然可用的,那些资源时过时需要重新请求的,当命中缓存之后,浏览器就会从硬盘中读取资源,虽然比从内存中读取慢了些,但比起网络请求还是快了不少的。绝大部分的缓存都来自disk cache.
凡是持久性存储都会面临容量增长的问题,disk cache也不例外。在浏览器自动清理时,会有神秘的算法去把“最老的”或者“最可能过时”的资源删除。
上述的缓存策略以及缓存/读取/失效的动作都是由浏览器内部判断 & 执行的, 我们只能设置响应头的某些字段来告诉浏览器,而不能自己操作。举个生活中去银行存/取钱的例子,你只能告诉银行职员,我要存/取多少钱,然后他们会经过一系列的记录和手续之后,把钱放到金库中去,或者从金库中拿出来给你。
Service Worker 给了我们另一种更加灵活,更加直接的操作方式。以上面的例子来说, 我们现在可以绕开银行职员,自己走到金库前, 自己把钱放进去再取出来,因此我们可以选择放那些钱(缓存那些文件),什么情况把钱取出来(路由匹配规则),取那些钱出来(缓存匹配并返回)。当然现实中银行没有给我们开放这样的服务,哈哈哈。
Service Worker能够操作的缓存有别于浏览器内部的memory cache 和 disk cache的,我们可以从Chorme的F12中的Application -> Cache Storage找到这个单独的“小金库”。除了位置不同之外,这个缓存是永久性的,即关闭Tab或者浏览器,下次打开依然还在。有两种情况导致这个缓存中的资源被清除;手动调用API cache.delete(resource)或者容量超过限制,被浏览器全部清空。 如果Service Worker没能命中缓存, 一般情况会使用fetch()方法继续获取资源,这时候浏览器就去mermory 或者 disk cache进行下一次找缓存工作了。
注意: 经过service worker的fetch()方法获取的资源,即使它并没有命中Service Worker缓存,甚至直接走了网络请求,也会被标注为from ServiceWorker。
请求网络
如果一个请求在上述3个位置(memory cache/disk cache/Service Worker)都没有找到缓存,那么浏览器会正式发送请求去获取内容。之后容易想到,为了提升之后请求的缓存命中率,自然要把这个资源加到缓存中去,具体来说:
1. 根据Service Worker中的handler决定是否存入Cache Storage
2. 根据HTTP头部相关的字段(Cache-control,Pragma等)决定是否存入disk cache
3. memory cache保存一份资源的引用,以备下次使用
按是失效策略分类
memory cache是浏览器为了加快读取缓存速度而进行的自身的优化行为,不受开发者控制,也不受HTTP协议头部的约束。
Server Worker是由开发者编写的额外的脚本,且缓存位置独立,出现也较晚,使用还不算太广泛。
平时最熟悉的其实是disk cache,它遵守HTTP协议头中的字段(强制缓存、对比缓存、Cache-Control等)都归于此类。
强制缓存的含义是,当客户端请求后,会先访问缓存数据库看缓存是否存在。如果存在则直接返回;不存在请求真的服务于,响应后再写入缓存数据库。 强制缓存直接减少请求数,是提升最大的缓存策略,可以造成强制缓存的字段是Cache-control和Expires
Expires: 这是HTTP1.0的字段,表示缓存到期时间,是一个绝对时间(当前时间+缓存时间)
Expires: Thu, 10 Nov 2020 07:00:00 GMT
在响应消息头中,设置这个字段之后,就可以告诉浏览器,未过期之前不需要再次请求,但是这个字段有两个缺点
1. 由于是绝对时间,用户可能会将客户端本地时间进行修改,导致浏览器判断缓存失效,或者是时差误差等饮食业也可能造成客户端和服务端时间不一致,倒是缓存失效。
2. 写法太复杂,表示时间的字符串多个空格,少个字母,都会导致非法字母失效。
已知Expires的缺点之后,在HTTP/1.1中增了Cache-control,该字段表示资源缓存的最大有效时间,在该时间内,客户端不需要向服务端发请求。
Cache-control:max-age= 2592999
Cache-control和Expires两者的区别是,前者是相对时间, 后者是绝对时间。 以下是Cache-control字段的常用值
- max-age: 既最大有效时间
- must-revalidate: 如果超过了max-age的时间,浏览器必须向服务器发送请求,验证资源是否有效。
- no-cache: 虽然字面意思是“不要缓存”,但实际上还是要求客户端缓存内容的,只是是否使用这个内容由后续的对比决定。
- no-store: 真正意义上的“不要缓存”。所有内容都不走缓存,包括强制和对比。
- pubilc: 所有的内容都可以被缓存(包括客户端和代理服务器,如CDN)
- private: 所有的内容只有客户端才可以缓存,代理服务器不能走缓存。默认值
注:这些值可以混合使用,例如Cache-control: public,max-age=259299; max-age 到期是应该重新验证。但实际情况以浏览器实现为准,max = 0, must-revalidate和no-cache是等价的。
总结一下,自从HTTP/1.1开始,Expires逐渐被Cache-control取代,Cache-control是一个相对时间,即使客户端时间发生改变相对时间也不会随之改变,优先级高于Expires,为了兼容HTTP/1.1和HTTP/1.0,实际项目中两个字段我们都会设置。
对比缓存(也叫协商缓存),当强制缓存失效,就需要使用对比缓存,由服务器缓存决定内容是否失效。 流程上说,浏览器先请求缓存数据库,返回一个缓存标识。之后浏览器拿着个标识和服务器通讯,如果缓存未失效,则返回状态码304表示继续使用,于是客户端继续使用缓存,则返回新的数据和缓存规则,浏览器响应数据后,再把规则写入到缓存数据库。对比缓存在请求数上和没有缓存是一致的,但如果是304的话,返回的仅仅是一个状态码而已,并没有实际的文件内容, 因此在响应体积上的节省是他的优点,通过减少响应体体积,来缩短网络传输时间。
对比缓存有两组字段:
Last-Modified & Modified-Since
- 服务器通过Last-Modified 字段告知客户端,资源最后一次修改的时间。
- 浏览器将这个值和内容一起在缓存数据库中
- 下一次请求相同资源时,浏览器从自己的缓存中找出“不确定是否过期的”缓存,因此在请求头中将上次的“Last-Modified ”的值写入到请求头的If-Modified-Since字段
- 服务器会将If-Modified-Since的值与Last-Modified 字段进行对比。如果相等则返回304;反之表示修改,响应200状态码,并返回数据
但是是有一定的缺陷的:
- 如果资源更新单位的速度是秒以下单位,那么该缓存是不能被使用的,因为他的时间单位最低时毫秒
- 如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成时间,尽管文件可能没有变化,所以不会起到缓存的作用
Etag & If-None-Match
为了解决上述缺陷,出现了一组新的字段Etag 和 If-None-Match
Etag存储的是文件的特殊标识(一般都是hash生成的),服务器存储着文件的Etag字段,之后的流程和Last-Modified一致,只是Last-Modified字段和它所表示的更新时间变成了Etag字段和它所表示的文件hash,把If-Modofied-Since变成了 If-None-Match。服务器进行比较,命中返回304,反之返回新资源和200.
Etag的优先级高于Last-Modefied
缓存小结
- 当浏览器要请求资源时
- 调用Serverice Worker 的 fetch 事件响应
- 查看memory cache
- 查看disk cache(如果有强制缓存且未失效,则使用强制缓存,不请求服务器,这时的状态码都是200; 如果有强制缓存已失效,使用对比缓存,比较后确定304还是200)
1. 发送网络请求,等待网络响应
2.把响应内容存入disk cache(如果HTTP头信息配置可以存的话)
3.把相应内容的引用存入memory cache
4.把相应内容存入Service Worker 的Cache Storage(如果Service Worker 的脚本调用了cache.put())