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