浏览器缓存一直是个老生常谈的话题,也是面试官常常用来鉴别面试者的利器,作为前端来讲这块知识是属于必须掌握的,再者利用好缓存也是做性能优化的有效方法。本文将从缓存原因、缓存读写顺序,缓存位置以及缓存策略这几个角度介绍浏览器缓存,并且最后给出实践的应用举例。
为什么要缓存
很多同学知道缓存的位置和字段,知道怎么用,但是你有没有想过为什么我们的页面需要浏览器缓存呢?
缓存可以减少用户等待时间,提升用户体验,直接从内存或磁盘中取缓存数据肯定是比从服务器请求更快的;减少网络带宽消耗:对于网站运营者和用户,带宽都代表着成本,过多的带宽消耗,都需要支付额外的费用。试想一下如果可以使用缓存,只会产生极小的网络流量,这将有效的降低运营成本。降低服务器压力:给网络资源设定有效期之后,用户可以重复使用本地的缓存,减少对源服务器的请求,降低服务器的压力。
缓存读写顺序
当浏览器对一个资源(比如一个外链的a.js)进行请求的时候会发生什么?请从缓存的角度大概说下:
调用ServiceWorker的fetch事件获取资源;查看memorycache;查看diskcache;这里又细分:如果有强制缓存且未失效,则使用强制缓存,不请求服务器。这时的状态码全部是;如果有强制缓存但已失效,使用协商缓存,比较后确定还是;发送网络请求,等待网络响应;把响应内容存入diskcache(如果请求头信息配置可以存的话);把响应内容的引用存入memorycache(无视请求头信息的配置,除了no-store之外);把响应内容存入ServiceWorker的CacheStorage(如果ServiceWorker的脚本调用了cache.put());上面这一系列过程其实是浏览器查找缓存和把资源存入缓存的执行流程。这其中出现了很多专业词汇,让人看了一脸懵逼,下面将从缓存位置和缓存策略两个角度简要介绍浏览器的缓存。
缓存位置
从浏览器开发者工具的Network面板下某个请求的Size中可以看到当前请求资源的大小以及来源,从这些来源我们就知道该资源到底是从memorycache中读取的呢,还是从diskcache中读取的,亦或者是服务器返回的。而这些就是缓存位置:
ServiceWorker
是一个注册在指定源和路径下的事件驱动worker;特点是:
运行在worker上下文,因此它不能访问DOM;独立于主线程之外,不会造成阻塞;设计完全异步,所以同步API(如XHR和localStorage)不能在ServiceWorker中使用;最后处于安全考虑,必须在HTTPS环境下才可以使用;说了这么多特点,那它和缓存有啥关系?其实它有一个功能就是离线缓存:ServiceWorkerCache;区别于浏览器内部的memorycache和diskcache,它允许我们自己去操控缓存;通过ServiceWorker设置的缓存会出现在浏览器开发者工具Application面板下的CacheStorage中。
memorycache
是浏览器内存中的缓存,相比于diskcache它的特点是读取速度快,但容量小,且时效性短,一旦浏览器tab页关闭,memorycache就将被清空。memorycache会自动缓存所有资源嘛?答案肯定是否定的,当HTTP头设置了Cache-Control:no-store的时候或者浏览器设置了Disabledcache就无法把资源存入内存了,其实也无法存入硬盘。当从memorycache中查找缓存的时候,不仅仅会去匹配资源的URL,还会看其Content-type是否相同。
diskcache
也叫HTTPcache是存在硬盘中的缓存,根据HTTP头部的各类字段进行判定资源的缓存规则,比如是否可以缓存,什么时候过期,过期之后需要重新发起请求吗?相比于memorycache的diskcache拥有存储空间时间长等优点,网站中的绝大多数资源都是存在diskcache中的。
浏览器如何判断一个资源是存入内存还是硬盘呢?关于这个问题,网上说法不一,不过比较靠谱的观点是:对于大文件大概率会存入硬盘中;当前系统内存使用率高的话,文件优先存入硬盘。
缓存按照缓存位置划分,其实还有一个HTTP/2的内容pushcache,由于目前国内对HTTP/2应用还不广泛,且网上对pushcache的知识还不齐全,所以本篇不打算介绍这块。
缓存策略
根据HTTPheader的字段又可以将缓存分成强缓存和协商缓存。强缓存可以直接从缓存中读取资源返回给浏览器而不需要向服务器发送请求,而协商缓存是当强缓存失效后(过了过期时间),浏览器需要携带缓存标识向服务器发送请求,服务器根据缓存标识决定是否使用缓存的过程。强缓存的字段有:Expires和Cache-Control。协商缓存的字段有:Last-Modified和ETag。
Expires
Expires是HTTP/1.0的字段,表示缓存过期时间,它是一个GMT格式的时间字符串。Expires需要在服务端配置(具体配置也根据服务器而定),浏览器会根据该过期日期与客户端时间对比,如果过期时间还没到,则会去缓存中读取该资源,如果已经到期了,则浏览器判断为该资源已经不新鲜要重新从服务端获取。由于Expires是一个绝对的时间,所以会局限于客户端时间的准确性,从而可能会出现浏览器判断缓存失效的问题。如下是一个Expires示例,是一个日期/时间:
Cache-Control
它是HTTP/1.1的字段,其中的包含的值很多:
max-age最大缓存时间,值的单位是秒,在该时间内,浏览器不需要向浏览器请求。这个设置解决了Expires中由于客户端系统时间不准确而导致缓存失效的问题;must-revalidate:如果超过了max-age的时间,浏览器必须向服务器发送请求,验证资源是否还有效;public响应可以被任何对象(客户端、代理服务器等)缓存;private响应只能被客户端缓存;no-cache跳过强缓存,直接进入协商缓存阶段;no-store不缓存任何内容,设置了这个后资源也不会被缓存到内存和硬盘;Cache-Control的值是可以混合使用的,比如:
当混合使用的时候它们的优先级如下图所示:
当Expires和Cache-Control都被设置的时候,浏览器会优先考虑后者。当强缓存失效的时候,则会进入到协商缓存阶段。具体细节是这样:浏览器从本地查找强缓存,发现失效了,然后会拿着缓存标识请求服务器,服务器拿着这个缓存标识和对应的字段进行校验资源是否被修改,如果没有被修改则此时响应状态会是,且不会返回请求资源,而是直接从浏览器缓存中读取。
而浏览器缓存标识可以是:Last-Modified和ETag:
Last-Modified
资源的最后修改时间;第一次请求的时候,响应头会返回该字段告知浏览器资源的最后一次修改时间;浏览器会将值和资源存在缓存中;再次请求该资源的时候,如果强缓存过期,则浏览器会设置请求头的If-Modifined-Since字段值为存储在缓存中的上次响应头Last-Modified的值,并且发送请求;服务器拿着If-Modifined-Since的值和Last-Modified进行对比。如果相等,表示资源未修改,响应;如果不相等,表示资源被修改,响应,且返回请求资源。如果资源更新的速度是小于1秒的,那么该字段将失效,因为Last-Modified时间是精确到秒的。所以有了ETag。
ETag
根据资源内容生成的唯一标识,资源是否被修改的判断过程和上面的一致,只是对应的字段替换了。Last-Modified替换成ETag,If-Modifined-Since替换成If-None-Match。
当Last-Modified和ETag都被设置的时候,浏览器会优先考虑后者。
浏览器的行为
浏览器地址栏输入URL后回车:查找diskcache中是否有匹配。如有则使用;如没有则发送网络请求。普通刷新(+R):因为TAB页并没有关闭,因此memorycache是可用的,会被优先使用(如果匹配的话),其次才是diskcache。强制刷新(++R):浏览器不使用缓存,因此发送的请求头部均带有Cache-control:no-cache(为了兼容,还带了Pragma:no-cache)。服务器直接返回和最新内容。当在开发者工具Network面板下设置了Disabledcache禁用缓存后,浏览器将不会从memorycache或者diskcache中读取缓存,而是直接发起网络请求。
缓存应用
静态资源
比如页面引入了一个JQuery,对于页面来说这个脚本就是一个工具库,基本上是不会发生变化的,对于这种资源可以将它的缓存时间设置得长一点,比如如下这个地址的脚本:
你会看到它的响应头里设置了,max-age=2590直接缓存30天:
频繁变化的资源
对于频繁变化的资源,比如某个页面经常需要调整,那么这个页面就需要在每次请求的时候都进行验证,可以在响应头这样设置:
不进行缓存
当然并不是所有请求都能被缓存,无法被浏览器缓存的请求如下:
HTTP信息头中包含Cache-Control:no-cache,pragma:no-cache(HTTP1.0),或Cache-Control:max-age=0等告诉浏览器不用缓存的请求;需要根据Cookie、认证信息等决定输入内容的动态请求是不能被缓存的;经过HTTPS安全加密的请求;POST请求无法被缓存;HTTP响应头中不包含Last-Modified/Etag,也不包含Cache-Control/Expires的请求无法被缓存;作者:大海我来了来源:掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。