浏览器缓存
说一说浏览器缓存。既然是浏览器缓存,那么就需要问一个问题:在浏览器里,从输入https://baidu.com
到页面展示,这中间的流程中,哪些步骤会发生资源缓存?
经验丰富且资深的面霸,对这个问题是不是有一种似曾相识的感觉?没错,这个提问如果换几个词就是:在浏览器里,从输入URL到页面展示,这中间发生了什么?
对于从输入URL到页面展示,这中间发生了什么这个问题,并不是这次的主题。我们这次讲的是发生了什么中的其中一(亿
)点点知识面:客户端缓存
拉回正题,让我们一起讨论下这个问题: 在浏览器里,从输入https://baidu.com
到页面展示,这中间的流程中,哪些步骤会发生资源缓存?
# 浏览器发起请求的整个流程
在开始之前,我们先了解一下浏览器发起请求的整个流程。从DNS查询到断开TCP连接,如下图:
图中告诉我们浏览器在请求资源之前会先进行DNS查询,DNS查询后建立TCP连接。当浏览器准备好这些之后,就开始和服务端通讯,会走HTTP的三次握手四次挥手流程,服务端响应。浏览器接收资源,然后浏览器断开TCP连接。
TCP连接
图中只是在表达HTTP请求流程。在实际应用场景中,TCP连接是长连接,所以并不会在每次请求资源之后都断开TCP连接
# DNS查询
浏览器在构建TCP连接之前会进行DNS查询。为什么要进行DNS查询?因为在虚拟的修仙世界中(互联网),无始大帝想要给小明邮寄一件商品。那么无始大帝是不是要知道小明的地址?在驿站填写快递信息的时候,需要注明小明的地址。而这个地址用什么表示?用IP表示。
我们常用的是IPv4地址,而IPv4的地址是一串数字。但是这串数字对于修仙人士来讲并不容易记住,我们更容易记住的是特定的一串字符。比如小明的道号叫百度,那么小明的域名是:baidu.com
。修仙人士虽然知道道号百度对应的IP,但是修仙世界的运行规则并不清楚道号百度对应的IP地址。那么,在上古时代的开荒先贤者们便对修仙世界的规则进行了定义,DNS便应运出生。
在我们对DNS有一个简单的概念之后,来一起看下浏览器拿到baidu.com
域名是怎么查询到IP的。
当浏览器第一次拿到baidu.com
之后,会问DNS服务baidu.com
对应的IP地址是什么。DNS服务返回baidu.com
对应的IP地址给浏览器,浏览器拿到IP地址后便构建TCP连接走HTTP流程。
当浏览器第二次又接收到baidu.com
之后,浏览器还会请求DNS服务吗?答案是:不会。所以,为什么不再请求DNS服务了?是浏览器它瓢了?
不是浏览器瓢了,而是浏览器在第一次拿到baidu.com
对应的IP地址后,会将这次的DNS查询结果缓存。那么下一次再接收到baidu.com
是不是可以直接从缓存中拿了?这时候就减少了一次DNS请求的通讯时间。
来看下浏览器DNS查询的具体流程,让我们更深刻的理解一下:如下图
DNS查询解释
想必大家比较关心DNS的查询结果是会放在哪里?其实DNS查询的流程应该不是只有浏览器和DNS服务。确切的说是客户端操作系统这边在维护DNS查询结果。浏览器在收到baidu.com
的时候,会和操作系统中的DNS模块通讯。那么剩下的DNS查询操作就交给操作系统来处理了,并且会将查询的结果缓存在系统磁盘上。
上图我们为了便于理解浏览器中的DNS查询服务,所以简化了客户端操作系统这一块。所以图中所示只是一个大概流程,并不是DNS查询的全部流程。
在浏览器中的DNS缓存这一块我们就先介绍到这。那么浏览器走完DNS查询后会去建立TCP链接,在建立TCP之后就会去请求资源。那么在请求资源这一块也是会发生浏览器缓存,并且缓存的控制也将由开发者告诉浏览器如何进行资源缓存
# 浏览器是如何缓存请求资源的?
我们可以将这个问题分解成三个小问题来看待浏览器是如何缓存请求资源的。
- 缓存过程
- 缓存的有效性
- 缓存数据的存放位置
在聊这三个小问题之前,我们先来一波缓存指令的知识点概念介绍。我们需要有一些前置的知识才能更全面的理解浏览器是如何缓存请求资源。
# 浏览器缓存设置
浏览器缓存设置即开发者将使用HTTP协议中定义的一些规范字段来告诉客户端如何进行缓存。那么具体的字段都有哪些呢?
# Expires
Expires 会出现在HTTP请求的响应头上,具体的格式为:expires: Wed, 21 Oct 2015 07:28:00 GMT
。它是HTTP1.0
的标准,值为一个时间戳,确切来说是一个格林尼治时间。Expires的值是用于告诉浏览器该资源的缓存时间。所以当Expires出现在HTTP请求的响应头中,那么表示该资源需要浏览器缓存,缓存的新鲜度则是Expires的值。
对于Expires来讲,它有一个问题是它的缓存时间是客户端操作系统的本地时间来判断的。所以,当用户主动去更改操作系统的本地时间时,那么极可能会发生一种结果: 即被缓存的资源新鲜度还在,由于本地时间被更改,浏览器这边判断缓存资源过期(新鲜度失效),那么将会走协商缓存流程。
Expires的总结
- http1.0的标准
- http响应头中设置Expires
- Expires的值为一个时间戳,用来表示资源被缓存的新鲜度
- 本地时间判断
# Pragma
Pragma 会出现在HTTP请求的响应头上,具体格式为: Pragma: no-cache
。它的值只有no-cache
,而no-cache
表达的含义和后面我们要说的cache-control
中出现的no-cache
指令的含义基本一致。它是HTTP1.0
的标准,但它的出现是用来向后兼容只支持HTTP1.0 协议的缓存服务器。那时候的HTTP1.1协议中的cache-control
还没有出来。
由于Pragma的效果依赖于不同的实现,所以在"请求-响应"链中可能会出现不同的效果。
Pragma总结
- HTTP1.0的标准
- 向后兼容只支持HTTP1.0协议的缓存服务器
- 只有一个值:
no-cache
,其含义和cache-control
中的no-cache
指令一致 - 不同的缓存服务器对于
Pragma
的实现效果不一致
# Cache-Control
Cache-Control是通用部首,通用部首的含义是它可以出现在HTTP请求头中也可以出现在HTTP响应头中。它是HTTP1.1的标准,也是我们目前web应用中最常见到的缓存控制资源。那么换一句话话说:我们现在常用的HTTP协议的版本为HTTP1.1。
Cache-Control字段中有4个我们常用的指令,分别为: no-cache
、no-store
、public
、private
。每个指令都有不同的应用场景,那么Cache-Control控制资源的新鲜度则是用Max-age
指令表示,Max-age
的值为一个资源缓存的时间,单位是秒。例如:max-age=2000
,那么则表示该资源的缓存时间为2000秒,在2000秒之内走强缓存,2000秒之后则走协商缓存策略。
需要注意的是,当HTTP请求中分别出现Expires
、Pragma
、Cache-Control
时,那么谁的缓存控制指令的权重最高?答案是Cache-Control
。也就是说,当HTTP请求中出现了上述三个控制缓存的标准时,客户端的浏览器会优先使用Cache-Control
字段
以上是Cache-control的基本概念, 我们接下来聊一聊Cache-Control中的4个指令
public
表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容。(
private
表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。私有缓存可以缓存响应内容,比如:对应用户的本地浏览器。
no-cache
在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证(协商缓存验证)。no-cache可能是我们会最常看到的缓存指令,它表示该缓存资源需要走协商缓存策略,但是协商缓存的触发条件是强缓存验证结果是否失效。
no-store
缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存。
Cache-Control总结
- HTTP1.1协议中定义的标准
- 通用部首
- 常用的4个控制缓存的指令:
no-cache
、no-store
、public
、private
- 4个控制缓存的指令概念介绍
- 常用控制缓存新鲜度的指令:
max-age=2000
- Cache-Control优先级最高
控制缓存新鲜度指令
上述介绍中,只说明了max-age
指令,并且指令的时间单位为秒。那么,控制缓存新鲜度的指令只有这一个吗?并不是,由于篇幅所限,这里并没有列出来。可以直接到MDN中查找Cache-Control相关资源新鲜度指令
# 辅助浏览器缓存的字段
在介绍了如何设置浏览器请求资源的缓存后,那么接下来需要说的概念是辅助浏览器缓存的字段。辅助浏览器缓存字段的出现是为了后续的协商缓存验证流程,而协商缓存验证的流程也会在辅助浏览器缓存的字段中一一道来,先来看辅助浏览器缓存的字段。
# ETag/If-None-Match
首先ETag,它表示资源文件修改的唯一标识符。也就说,当资源文件被修改的时候,那么服务器端将会重新生成该资源的唯一标识符。
ETag的唯一标识符会让缓存更高效,并节省HTTP传输带宽。为什么这样说呢?当我们的缓存资源在强缓存校验失效后,便会走协商缓存验证,而协商缓存验证需要和服务器通讯。
那么,在这次的协商缓存HTTP通讯过程中,在请求头中会携带If-None-Match
字段,If-None-Match
字段的值则是ETag
的值。服务端接收到请求头后,会拿If-None-Match的值与服务器上的资源文件的ETag做对比,如果对比结果一致,那么协商缓存验证通过,接口返回304状态码,并且响应体是不会携带任何数据的。如果协商缓存验证失效,那么接口返回200状态码,响应体返回最新的资源数据。
上述流程中,当协商缓存验证通过时,HTTP请求的响应体是不会携带资源文件数据的。所以,对于HTTP通讯来讲,确实在节省传输带宽并且让请求时间更快。
ETag/If-None-Match总结
- 唯一标识符
- ETag会让缓存更高效,并节省HTTP传输带宽
- ETag是出现在HTTP响应头中
- If-None-Match携带的值是ETag的值
- If-None-Match是出现在HTTP请求头中
# Last-Modified/If-Modified-Since
Last-Modified与ETag对资源缓存的控制基本一致,它是资源文件在服务器上的最后一次修改的日期及时间,时间具体到秒。所以它的精度比ETag要低。
Last-Modified有可能发生资源文件以纳秒或者毫秒修改的时候,当走协商缓存验证的时候并不会认为资源文件已经修改了。真实情况是确实被修改了。
If-Modified-Since是出现在HTTP请求头中,携带的值是Last-Modified的值,并且If-Modified-Since只可用在GET和HEAD请求中。
Last-Modified/If-Modified-Since总结
- Last-Modified是表示资源文件的最后一次时间,单位为秒
- Last-Modified是出现在HTTP响应头中
- Last-Modified比ETag的精度要低
- If-Modified-Since携带的值是Last-Modified的值
- If-Modified-Since是出现在HTTP请求头中
- If-Modified-Since只可用在GET和HEAD请求中
ETag和Last-Modified的优先级
我在MDN中查到的资料是Last-Modified的精度比ETag低,所以这是一个备用机制。那么可以认为当HTTP请求头中同时出现If-None-Match和If-Modified-Since时,服务端会优先判断If-None-Match。所以优先级就很明确了,ETag的使用优先级会比Last-Modified高。
# 缓存资源的过程
在了解完以上控制浏览器资源缓存的相关概念后,那么我们开始聊一聊浏览器缓存的过程。在浏览器缓存的过程中会发生两种缓存策略的验证。
浏览器首先会对资源文件进行强缓存验证,如果强缓存验证通过,则直接从缓存区拿数据且HTTP响应码为200,不会发送HTTP请求到服务端。如果强缓存验证失效,那么会走协商缓存验证。
当浏览器走协商缓存策略时,浏览器会发送HTTP请求到服务端,并且HTTP请求头中会携带ETag或者Last-Modified的值。当服务端接收到请求头后,会走ETag或者Last-Modified的验证。如果服务端这边验证通过,那么HTTP响应码为304并且请求体中是没有任何资源文件数据。当浏览器收到协商缓存验证结果为资源文件未更新,那么浏览器会继续刷新缓存区文件的新鲜度。
如果ETag或者Last-Modified验证的结果为资源文件已更新,那么HTTP的响应码为200,请求体中返回最新的资源文件数据。浏览器拿到最新的资源文件后会更新缓存区的文件新鲜度。然后浏览器这边开始处理数据、渲染数据。
当我们了解完整个缓存过程后,发现这里面提到了两个概念:强缓存
和协商缓存
# 强缓存介绍
强缓存是当我们访问URL的时候,不会向服务器发送请求,直接从缓存中读取资源,但是会返回200的状态码
# 协商缓存介绍
协商缓存就是强缓存失效后,浏览器带上标识向服务器发送请求, 服务器根据缓存标识来决定是否使用缓存, 这一过程是协商缓存。
划重点
- 在命中协商缓存前是必走强缓存验证的。只有当强缓存验证资源失效的时候,才会走协商缓存验证
- 协商缓存验证是和服务端进行协商的,所以需要携带资源文件标识符给服务端
- 协商缓存验证会有两种结果:
- 缓存生效,HTTP状态码返回304且响应体无任何资源文件数据
- 缓存失效,HTTP状态码返回200且响应体中返回最新的资源文件
在读完整个浏览器缓存过程的文字后,我们一起来更直白的看下图中所表达的浏览器缓存过程
# 缓存数据的存放位置
浏览器在拿到缓存资源后,会将缓存资源存放在哪里呢?那么一起来看下缓存会具体存放在哪里吧
# Service Worker
Service Worker是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用Service Worker,传输协议必须为HTTPS,因为Service Worker中涉及到请求拦截,所以必须使用HTTPS协议来保障安全。
划重点
Service Worker的缓存与浏览器其他内建的缓存机制不同,它可以让我自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的
# Memory Cache
内存缓存,主要包含当前页面中已经抓取到的资源,例如页面上的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然高效,但是缓存持续性很短,会随着进程的释放而释放。并且内存缓存的容量相对于磁盘缓存要小的很多,所以并不是任何资源都会放在内存缓存中。Prefetch cache 的缓存存放位置就是内存
# Disk Cache
存储在磁盘上的缓存,读取速度相对会慢点。但是任何资源都可以存储到磁盘中,比之Memory Cache胜在容量和存储时效性上。在浏览器所有缓存中,Disk Cache覆盖面积是最大的。
# Push Cache
Push Cache(推送缓存)是HTTP/2中的内容,当以上三种缓存都没有命中时,他才会被使用。 它只在会话(Session)中存在,并且缓存时间会很短。 在Chrome浏览器中大概是5分钟左右。同时它也并非严格执行HTTP头中的缓存指令。
# 总结
# 缓存最佳实践
当我们清楚强缓存和协商缓存规则后,那么是不是希望将资源都尽量的命中强缓存呢?那么我们来设想这样一个场景:
有一个JS文件,它的更改并不是很频繁,有可能是两三天更改一次。如果设置缓存的过期时间的值太小,比如才设置2000秒。那么在资源文件没有更改的情况下就要频繁的走协商缓存验证。协商缓存总归是要想服务器发送请求的,所以这明显不合适。
如果设置JS文件的过期时间的值太大,有会存在另一个问题。当资源文件在服务器上被更新的时候,浏览器这边的资源文件的有效期还是有效的。那么用户就不可能拿到最新的资源文件。所以,我们应该处理这个场景呢?
处理这种场景的方式可以这样:
将HTML的资源文件设置为协商缓存(cache-control:no-cache;max-age=0
),将网页中要用的到的JS、CSS、字体、图片等资源通过在HTML的head头里设置link
的方式来加载文件资源。
也就是说,用户每次访问web应用的时候,HTML资源文件都会走协商验证。如果HTML资源文件有任何更改,那么服务器就会返回最新的HTML资源。如果HTML资源没有更新,那么浏览器将会使用之前就被缓存的JS、CSS、字体、图片等文件。
# 何时使用内存缓存?何时使用磁盘缓存?
对于这个问题,我也挺疑惑的。用谷歌查阅了不少文章资料,但都众说纷纭。不过大部分的意见是:
- 对于当前的页面的资源文件,会优先存储在内存中。如果内存中存放不下,那么会将资源存放在磁盘缓存上。
- 如果当时客户端的操作系统内存不足的情况下,浏览器会放弃将资源存放在内存中。而是优选选择磁盘存储。
# 查找缓存的优先级?
这个优先级为:浏览器会优先查找 -> Service Worker -> Memory Cache -> Disk Cache -> Push Cache
# JS有设置磁盘缓存的方式吗?(送分题)
其实这个不用考虑的,JS是有设置磁盘缓存的方式。例如:localStorage、Cookie、IndexedDB
# 扩展
# web缓存种类
其实WEB缓存的种类不光客户端浏览器这一种缓存方式。还有其他的一些缓存,分别是:
- 数据库缓存,例如:Redis
- CND缓存
- 代理服务器缓存
如果有大佬对其他缓存种类感兴趣,可以自己谷歌查阅资料和文章。然后我们再一起讨论讨论、学习学习。
# HTTP协议中还有其他设置缓存的字段吗?
答案肯定是有的,文中也只是介绍了设置缓存常见的一些字段。如果细心的同学读完文章后会发现一个问题:这个HTTP请求的资源中并没有出现文章中所说控制缓存的这三个字段Expires
、Pragma
、Cache-Control
。为什么该资源还是被缓存了?对于浏览器的实现来讲,浏览器不光是只依据HTTP协议中所规定的设置缓存字段,浏览器本身也实现了一些对资源文件的缓存算法。我也只听说过启发式算法
。
当然,对于HTTP协议来讲,也有其他的一些字段也是用于控制资源缓存或者协助资源缓存。比如:vary
# 最后
文中的一些观点和见解只是我个人在查阅一些资料和文章时的理解。所以,出错,必不可免。如果错误,欢迎大佬指正交流。如果有不同的想法,欢迎大佬一起交流,或许是我的表达方式也会给大佬们带来一些误解,也欢迎大佬们指正交流。