浏览器缓存类型
浏览器缓存的作用和好处无需多言,其根据是否需要向服务器重新发起 http 请求分为强缓存和协商缓存两种类型。强缓存的优先级要高于协商缓存。
强缓存
强缓存机制下浏览器首先查找本地缓存,如果命中则不会向服务器发起请求。此时返回 200 状态码,并带有 from disk cache 或 from memory cache 字样。强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control。
Memory Cache
Memory Cache 也就是内存中的缓存,主要包含的是当前页面中已经获取到的资源,比如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续很短,会随着进程的释放而释放。一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?这是不可能的。计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。
Disk Cache
Disk Cache 也就是存储在磁盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。绝大部分的缓存都来自 Disk Cache。
浏览器会把哪些文件丢进内存中?哪些丢进磁盘中?关于这点,网上说法不一,不过以下观点比较靠得住:
- 对于大文件来说,大概率是不存储在内存中的,反之优先;
- 当前系统内存使用率高的话,文件优先存储进磁盘。
Expires
Expires 用来指定缓存过期时间,是服务器端发的具体时间点。也就是说,Expires=请求时间 + max-age。Expires 是服务器响应消息头字段,在响应 http 请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存读取数据,而无需再次请求。
Expires 是 HTTP/1.0 的产物,受限于本地时间。如果修改了本地时间,可能会造成缓存失效。比如 Expires: Wed, 22 Oct 2018 08:30:00 GMT表示资源会在 Wed, 22 Oct 2018 08:30:00 GMT 后过期,需要再次请求。
Cache-Control
在 HTTP/1.1中,Cache-Control 是最重要的规则,主要用于控制网页缓存。比如当 Cache-Control:max-age=300时,则代表在这个请求正确返回时间(浏览器会记录下来)的5分钟内再次加载资源,就会命中强缓存。我们重点看看如下几个设置值:
public
所有内容都将被缓存(客户端和代理服务器都可缓存)。具体来说响应可被任何中间节点缓存,如 Browser <-- proxy <-- Server,中间的 proxy 可以缓存资源,比如下次再请求同一资源时 proxy 直接把自己缓存的内容给 Browser 而不再向 Server 要。
private
所有内容只有客户端可以缓存,Cache-Control 的默认取值。具体来说,表示中间节点不允许缓存,对于 Browser <-- proxy <-- Server,proxy 会老老实实把 Server Browser,自己不缓存任何数据。当下次 Browser 再次请求时 proxy 会做好请求转发而不是自作主张给自己缓存的数据。
no-cache
客户端缓存内容,是否使用缓存则需要经过协商缓存来验证决定。表示不使用 Cache-Control 的缓存控制方式做前置验证,而是使用 Etag 或者 Last-Modified 字段来控制缓存。需要注意的是,no-cache 这个名字有一点误导。设置了 no-cache 之后,并不是说浏览器就不再缓存数据,只是浏览器在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致。
no-store
所有内容都不会被缓存,既不使用强缓存,也不使用协商缓存。
max-age
max-age=xxx(xxx is numeric) 表示缓存内容将在 xxx 秒后失效。
Expires 和 Cache-Control 两者对比
其实这两者差别不大,区别就在于 Expires 是 HTTP/1.0 的产物,Cache-Control 是 HTTP/1.1 的产物。两者同时存在的话,Cache-Control 优先级高于 Expires。在某些不支持 HTTP1.1 的环境下,Expires 就会发挥用处。所以 Expires 其实是过时的产物,现阶段它的存在只是一种兼容性的写法。
强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?此时我们需要用到协商缓存。
协商缓存
协商缓存就是强缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:
- 协商缓存生效,返回 304 和 Not Modified。
- 协商缓存失效,返回 200 和请求结果。
协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag。
Last-Modified 和 If-Modified-Since
浏览器在第一次访问资源时,服务器返回资源的同时,在 response header 中添加 Last-Modified 的 header,值是这个资源在服务器上的最后修改时间,浏览器接收后缓存文件和header。
Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT
浏览器下一次请求这个资源,浏览器检测到有 Last-Modified 这个 header,于是添加 If-Modified-Since 这个 header,值就是 Last-Modified 中的值。服务器再次收到这个资源请求,会根据 If-Modified-Since 中的值与服务器中这个资源的最后修改时间对比,如果没有变化,返回 304 和空的响应体,浏览器直接从缓存读取。如果 If-Modified-Since 的时间小于服务器中这个资源的最后修改时间,说明文件有更新,则返回新的资源文件和 200。
ETag 和 If-None-Match
Etag 是服务器响应请求时返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag 就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的 Etag 值放到 request header 里的 If-None-Match 里,服务器只需要比较客户端传来的 If-None-Match 跟自己服务器上该资源的 ETag 是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现 ETag 匹配不上,那么直接以常规 GET 200 回包形式将新的资源(当然也包括了新的 ETag)发给客户端。如果 ETag 是一致的,则返回 304 指示客户端直接使用本地缓存即可。
Last-Modified 和 ETag 两者对比
- 在精确度上,Etag 要优于 Last-Modified。Last-Modified 的时间单位是秒,如果某个文件在 1 秒内改变了多次,那么他们的 Last-Modified 其实并没有体现出来修改。但是 Etag 每次都会改变确保了精度。如果是负载均衡的服务器,各个服务器生成的 Last-Modified 也有可能不一致。
- 在性能上,Etag 要逊于 Last-Modified,毕竟 Last-Modified 只需要记录时间,而 Eta g需要服务器通过算法来计算出一个 hash 值。
- 在优先级上,服务器校验优先考虑 Etag。
缓存机制
强缓存优先级高于协商缓存,若强缓存(Expires 和 Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since 和 Etag / If-None-Match)。协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回 200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回 304,继续使用缓存。
那如果什么缓存策略都没设置,浏览器会怎么处理呢?对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。
前端 SPA 应用缓存策略
首页 index.html 缓存策略
由于默认什么都不设置的情况下会应用强缓存,这样就会导致每次 index.html 有更新时用户端都不一定能及时得到最新版本(经常需要用户强制刷新或手动清除浏览器缓存)。建议 index.html 中设置 Cache-Control 值为 no-cache 取消强缓存,使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来协商验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小且兼顾了及时更新的需求,另外由于只有 index.html 这一个单页需要这样,因此这种方式明显利大于弊。
静态资源文件缓存策略
静态资源文件如 css js 文件等,由于前端构建打包输出中对于这些文件会引入 hash 动态值,那么每次有更新时加载他们的 URL 也都不相同。因此对于这些文件,我们完全可以设置强缓存 Cache-Control 为一个很大的值,比如半年或一年(以 nginx 举例来说,可以设置 expires 180d 等),在此期间内浏览器无需发起任何请求,直接使用本地缓存,最大化利用浏览器缓存。而在这些文件有更新的时候,由于请求 URL 也变了,所以无需担心浏览器不能及时得到最新版本。