1.能不能说一说浏览器缓存?
缓存是性能优化中非常重要的一环,浏览器的缓存机制对开发也是非常重要的知识点。
接下来以三个部分来把浏览器的缓存机制说清楚:
强缓存
协商缓存
缓存位置
强缓存
浏览器中的缓存作用分为两种情况,一种是需要发送HTTP请求,一种是不需要发送。
首先是检查强缓存,这个阶段不需要发送HTTP请求。
在HTTP/1.0和HTTP/1.1当中,这个字段是不一样的。
在早期,也就是HTTP/1.0时期,使用的是Expires,而HTTP/1.1使用的是Cache-Control。
让我们首先来看看Expires。
Expires
Expires即过期时间,存在于服务端返回的响应头中,告诉浏览器在这个过期时间之前可以直接从缓存里面获取数据,无需再次请求。
比如下面这样
Expires: Wed, 22 Nov 2019 08:41:00 GMT
表示资源在2019年11月22号8点41分过期,过期了就得向服务端发请求。
这个方式看上去没什么问题,合情合理,但其实潜藏了一个坑,那就是服务器的时间和浏览器的时间可能并不一致,那服务器返回的这个过期时间可能就是不准确的。
因此这种方式很快在后来的HTTP1.1版本中被抛弃了。
Cache-Control
在HTTP1.1中,采用了一个非常关键的字段:Cache-Control。
这个字段也是存在于服务端返回的响应头中
它和Expires本质的不同在于它并没有采用具体的过期时间点这个方式,而是采用过期时长来控制缓存,对应的字段是max-age。
比如这个例子:
Cache-Control:max-age=3600
代表这个响应返回后在 3600 秒,也就是一个小时之内可以直接使用缓存。
如果你觉得它只有max-age一个属性的话,那就大错特错了。
它其实可以组合非常多的指令,完成更多场景的缓存判断, 将一些关键的属性列举如下:
public: 客户端和代理服务器都可以缓存。因为一个请求可能要经过不同的代理服务器最后才到达目标服务器,那么结果就是不仅仅浏览器可以缓存数据,中间的任何代理节点都可以进行缓存。
private: 这种情况就是只有浏览器能缓存了,中间的代理服务器不能缓存。
no-cache: 跳过当前的强缓存,发送HTTP请求,即直接进入协商缓存阶段。
no-store:非常粗暴,不进行任何形式的缓存。
s-maxage:这和max-age长得比较像,但是区别在于s-maxage是针对代理服务器的缓存时间。
值得注意的是,当Expires和Cache-Control同时存在的时候,Cache-Control会优先考虑。
当然,还存在一种情况,当资源缓存时间超时了,也就是强缓存失效了,接下来怎么办?没错,这样就进入到第二级屏障——协商缓存了。
协商缓存
强缓存失效之后,浏览器在请求头中携带相应的缓存tag来向服务器发请求,由服务器根据这个tag,来决定是否使用缓存,这就是协商缓存。
具体来说,这样的缓存tag分为两种: Last-Modified 和 ETag。
这两者各有优劣,并不存在谁对谁有绝对的优势,跟上面强缓存的两个 tag 不一样。
Last-Modified
即最后修改时间。在浏览器第一次给服务器发送请求后,服务器会在响应头中加上这个字段。
浏览器接收到后,如果再次请求,会在请求头中携带If-Modified-Since字段,这个字段的值也就是服务器传来的最后修改时间。
服务器拿到请求头中的If-Modified-Since的字段后,其实会和这个服务器中该资源的最后修改时间对比:
如果请求头中的这个值小于最后修改时间,说明是时候更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。
否则返回304,告诉浏览器直接用缓存。
ETag
ETag 是服务器根据当前文件的内容,给文件生成的唯一标识,只要里面的内容有改动,这个值就会变。
服务器通过响应头把这个值给浏览器。
浏览器接收到ETag的值,会在下次请求时,将这个值作为If-None-Match这个字段的内容,并放到请求头中,然后发给服务器。
服务器接收到If-None-Match后,会跟服务器上该资源的ETag进行比对:
如果两者不一样,说明要更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。
否则返回304,告诉浏览器直接用缓存。
两者对比
1.在精准度上,ETag优于Last-Modified。
优于 ETag 是按照内容给资源上标识,因此能准确感知资源的变化。
而 Last-Modified 就不一样了,它在一些特殊的情况并不能准确感知资源变化,
主要有两种情况:
编辑了资源文件,但是文件内容并没有更改,这样也会造成缓存失效。
Last-Modified 能够感知的单位时间是秒,如果文件在 1 秒内改变了多次,那么这时候的 Last-Modified 并没有体现出修改了。
2.在性能上,Last-Modified优于ETag,也很简单理解
Last-Modified仅仅只是记录一个时间点,而 Etag需要根据文件的具体内容生成哈希值。
另外,如果两种方式都支持的话,服务器会优先考虑ETag。
缓存位置
前面我们已经提到,当强缓存命中或者协商缓存中服务器返回304的时候
我们直接从缓存中获取资源。那这些资源究竟缓存在什么位置呢?
浏览器中的缓存位置一共有四种,按优先级从高到低排列分别是:
Service Worker
Memory Cache
Disk Cache
Push Cache
Service Worker
Service Worker 借鉴了 Web Worker的 思路,即让 JS 运行在主线程之外,由于它脱离了浏览器的窗体,因此无法直接访问DOM。
虽然如此,但它仍然能帮助我们完成很多有用的功能,比如离线缓存、消息推送和网络代理等功能。其中的离线缓存就是 Service Worker Cache。
Service Worker 同时也是 PWA 的重要实现机制,关于它的细节和特性,我们将会在后面的 PWA 的分享中详细介绍。
Memory Cache 和 Disk Cache
Memory Cache指的是内存缓存,从效率上讲它是最快的。
但是从存活时间来讲又是最短的,当渲染进程结束后,内存缓存也就不存在了。
Disk Cache就是存储在磁盘中的缓存,从存取效率上讲是比内存缓存慢的,但是他的优势在于存储容量和存储时长。
既然两者各有优劣,那浏览器如何决定将资源放进内存还是硬盘呢?主要策略如下:
比较大的JS、CSS文件会直接被丢进磁盘,反之丢进内存
内存使用率比较高的时候,文件优先进入磁盘
Push Cache
即推送缓存,这是浏览器缓存的最后一道防线。
它是 HTTP/2 中的内容,虽然现在应用的并不广泛,但随着 HTTP/2 的推广,它的应用越来越广泛。
总结
对浏览器的缓存机制来做个简要的总结:
首先通过 Cache-Control 验证强缓存是否可用
如果强缓存可用,直接使用
否则进入协商缓存,即发送 HTTP 请求,服务器通过请求头中的If-Modified-Since或者If-None-Match字段检查资源是否更新
若资源更新,返回资源和200状态码
否则,返回304,告诉浏览器直接从缓存获取资源
2. 能不能说一说浏览器的本地存储?各自优劣如何?
浏览器的本地存储主要分为Cookie、WebStorage和IndexedDB,
其中WebStorage又可以分为localStorage和sessionStorage。
接下来我们就来一一分析这些本地存储方案。
Cookie
Cookie 最开始被设计出来其实并不是来做本地存储的,而是为了弥补HTTP在状态管理上的不足。
HTTP 协议是一个无状态协议,客户端向服务器发请求,服务器返回响应,下次发请求如何让服务端知道客户端是谁呢?这种背景下,就产生了 Cookie.
Cookie 本质上就是浏览器里面存储的一个很小的文本文件,内部以键值对的方式来存储(在chrome开发者面板的Application这一栏可以看到)。
向同一个域名下发送请求,都会携带相同的 Cookie,服务器拿到 Cookie 进行解析,便能拿到客户端的状态。
Cookie 的作用很好理解,就是用来做状态存储的,但它也是有诸多致命的缺陷的:
1.容量缺陷。Cookie 的体积上限只有4KB,只能用来存储少量的信息。
2.性能缺陷。Cookie 紧跟域名,不管域名下面的某一个地址需不需要这个 Cookie ,请求都会携带上完整的 Cookie,
这样随着请求数的增多,其实会造成巨大的性能浪费的,因为请求携带了很多不必要的内容
3.安全缺陷。由于 Cookie 以纯文本的形式在浏览器和服务器中传递,很容易被非法用户截获,然后进行一系列的篡改,
在 Cookie 的有效期内重新发送给服务器,这是相当危险的。另外,在HttpOnly为 false 的情况下,Cookie 信息能直接通过 JS 脚本来读取。
localStorage
和Cookie异同
localStorage有一点跟Cookie一样,就是针对一个域名,即在同一个域名下,会存储相同的一段localStorage。
不过它相对Cookie还是有相当多的区别的:
1.容量。localStorage 的容量上限为5M,相比于Cookie的 4K 大大增加。
当然这个 5M 是针对一个域名的,因此对于一个域名是持久存储的。
2.只存在客户端,默认不参与与服务端的通信。
这样就很好地避免了 Cookie 带来的性能问题和安全问题。
3.接口封装。通过localStorage暴露在全局,并通过它的 setItem 和 getItem等方法进行操作,非常方便。
操作方式
接下来我们来具体看看如何来操作localStorage。
let obj = { name: "sanyuan", age: 18 };
localStorage.setItem("name", "sanyuan");
localStorage.setItem("info", JSON.stringify(obj));
接着进入相同的域名时就能拿到相应的值:
let name = localStorage.getItem("name");
let info = JSON.parse(localStorage.getItem("info"));
从这里可以看出,localStorage其实存储的都是字符串,
如果是存储对象需要调用JSON的stringify方法,并且用JSON.parse来解析成对象。
应用场景
利用localStorage的较大容量和持久特性,可以利用localStorage存储一些内容稳定的资源,
比如官网的logo,存储Base64格式的图片资源,因此利用localStorage
sessionStorage
特点
sessionStorage以下方面和localStorage一致:
1.容量。容量上限也为 5M。
2.只存在客户端,默认不参与与服务端的通信。
3.接口封装。除了sessionStorage名字有所变化,存储方式、操作方式均和localStorage一样。
但sessionStorage和localStorage有一个本质的区别,
那就是前者只是会话级别的存储,并不是持久化存储。
会话结束,也就是页面关闭,这部分sessionStorage就不复存在了。
应用场景
可以用它对表单信息进行维护,将表单信息存储在里面,可以保证页面即使刷新也不会让之前的表单信息丢失。
可以用它存储本次浏览记录。
如果关闭页面后不需要这些记录,用sessionStorage就再合适不过了。
事实上微博就采取了这样的存储方式。
IndexedDB
IndexedDB是运行在浏览器中的非关系型数据库, 本质上是数据库,
绝不是和刚才WebStorage的 5M 一个量级,理论上这个容量是没有上限的。
关于它的使用,本文侧重原理,而且 MDN 上的教程文档已经非常详尽,这里就不做赘述了,感兴趣可以看一下使用文档。
接着我们来分析一下IndexedDB的一些重要特性,除了拥有数据库本身的特性,
比如支持事务,存储二进制数据,还有这样一些特性需要格外注意:
1.键值对存储。内部采用对象仓库存放数据,在这个对象仓库中数据采用键值对的方式来存储。
2.异步操作。数据库的读写属于 I/O 操作, 浏览器中对异步 I/O 提供了支持。
3.受同源策略限制,即无法访问跨域的数据库。
总结
浏览器中各种本地存储和缓存技术的发展,给前端应用带来了大量的机会,
PWA 也正是依托了这些优秀的存储方案才得以发展起来。
重新梳理一下这些本地存储方案:
1.cookie并不适合存储,而且存在非常多的缺陷。
2.Web Storage包括localStorage和sessionStorage, 默认不会参与和服务器的通信。
3.IndexedDB为运行在浏览器上的非关系型数据库,为大型数据的存储提供了接口。