基于上一篇文章对于Nginx的介绍,我们明白Nginx可以用来配置浏览器如何对项目的文件资源进行缓存。
在此,就不得不介绍浏览器的缓存机制是如何工作的。
1. webkit资源的分类
webkit的资源分类主要分为两大类:主资源和派生资源。
1.1 WebKit
首先明白WebKit这个概念,WebKit是浏览器吗?
WebKit不是一个完整的Web浏览器,而是一个开源的浏览器引擎,用于渲染网页内容。
这个引擎负责解释HTML、CSS和JavaScript代码,并将它们转换为用户能够看到的图形界面。因此,WebKit是许多流行浏览器的基础。
1.2 主资源
主资源,指启动页面加载过程的初始资源。
在大多数情况下,主资源是一个HTML文档。
因为它通常是第一个被浏览器请求和解析的文件,它定义了页面的结构和内容。
例如,当你访问网站 http://example.com/index.html 时,index.html 就是主资源。
1.3 派生资源
指被主资源通过各种链接间接引入的资源(如 <img>、 <script>、<link> 等标签)。
这些资源对构建完整的网页是必需的,但它们并不是直接被用户请求的文件。
例如,index.html 可能包含了以下标签:
<img src="logo.png">:这那么,这张名为 logo.png 的图片会作为派生资源被加载到页面上。
<link rel="stylesheet" href="style.css">:这个名为 style.css 的CSS样式表会作为派生资源被加载。
<script src="script.js"></script>:这个名为 script.js 的JS文件会作为派生资源被加载。
以上的logo.png、style.css 和 script.js 都是派生资源,因为它们都不是直接访问的URL,而是由主资源间接引入的。
1.4 对主资源和派生资源的不同缓存机制和加载优先级
两者的区分对于缓存策略、页面加载性能优化、以及浏览器的工作方式都有重要影响。
浏览器会对主资源和派生资源分别采取不同的缓存机制和加载优先级。
1.4.1 缓存机制的不同
对于HTML页面,浏览器可能使用缓存验证机制来检查是否有更新,而非每次都重新下载整个页面。
对于派生资源,浏览器也有缓存机制,并且浏览器可能会对派生资源使用更长的缓存时间,因为这些资源的更新频率通常低于HTML页面。
1.4.2 加载优先级的不同
浏览器会优先加载主资源,因为HTML页面是页面结构的入口点。
而图片、视频和字体等派生资源的加载优先级通常低于主资源,因为它们不影响页面的基本布局和功能。
2. 什么是缓存
指浏览器对之前请求过的资源进行缓存,以便下一次访问该资源时可以重复使用。
好处就是从缓存读取资源,比向服务端发HTTP请求回来拿到的资源要快很多,提高页面访问速度,节省带宽,降低服务器压力。
3. 浏览器如何判断是否使用缓存
3.1 强制缓存
在已缓存的资源未过期的情况下,不会向服务器发送请求,会直接从缓存中读取资源。
对于强制缓存而言,响应头中有两个字段标明了它的缓存规则:一个是Expires,另一个是 Cache - Control。
3.1.1 Expires
Expires的值包含具体的日期和时间,是服务器返回的资源到期时间。
即在这个时间之前,可以直接从缓存资源池中读取资源,无需再次请求服务器。
例子:
Expires: Wed, 31 Jul 2019 02:58:24 GMT
3.1.2 Cache-Control
Cache-Control 可以被用于请求头和响应头中,组合使用多种指令来指定缓存机制。
下面列举了比较常用的指令:
(1)private:资源只可以被客户端缓存,Cache-Control 的默认值。
(2)public:资源可以被客户端和代理服务器缓存。
(3)max-age=t:客户端缓存的资源将在t秒后失效,失效后走协商缓存。
(4)no-cache:跳过强缓存,需要使用协商缓存来验证资源
(5)no-store:不缓存任何资源。
3.1.2.1 private和public的区别
private和public的区别在于是否允许中间节点(即代理服务器)进行资源缓存。
(1)如果Cache-Control 值设置为public:客户端 - 代理服务器 - 服务器
那么中间的代理服务器可以缓存资源,如果下一次再请求同一资源,代理服务器就可以直接把自己缓存的资源返回给客户端,而无需再请求服务器。
代理服务器缓存的资源可以共享给不同用户。
(2)如果 Cache-Control 值设置为 private,表明资源只能被当前用户在客户端缓存。
属于私有缓存,不能作为共享缓存。
例如:
Cache-Control: max-age=31536000
Cache-Control 仅指定了 max-age,所以默认为 private,缓存时间为 31536000 秒(365天)
也就是说,在 365 天内再次请求该资源时,都会直接使用资源池中的资源。”、
3.1.3 Expires与Cache-Control 同时出现
Expires与Cache-Control 同时出现时,Cache-Control 的优先级高于Expires。
因为Expires 是有问题的,它最大的问题在于对“本地时间”的依赖。
服务端和客户端的时间设置有可能不一样,或者我们直接手动去把客户端的时间改掉,那么这种情况下expires将无法到达我们的预期效果,因此需要使用Cache-Control来弥补缺陷。
Expires 属于 HTTP/1.0 的产物,而 Cache-Control 属于 HTTP/1.1 的产物,如果服务器同时设置了Expires 与 Cache-Control,那么以更先进的 Cache-Control 为准。
在某些不支持 HTTP/1.1 的环境中,Expires 才能发挥作用,现阶段只是一种兼任性的写法。
3.1.4 强制缓存的弊端
是否走强制缓存,主要取决于判断该缓存资源是否过期,而非关心该资源对应的服务端是否已经将其更新到最新,这可能会导致上一次加载并缓存的资源早已不是最新的内容。
3.2 协商缓存
针对强制缓存的弊端,协商缓存需要进行资源对比判断是否可以使用缓存。
客户端第一次请求资源时,服务器会将资源与该资源的缓存标识一起返回给客户端,客户端将二者备份至资源池中。
当再次请求相同资源时(此时,强缓存发现缓存资源过期),客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行验证。
如果验证结果为未更新,服务器会返回304,则客户端继续使用缓存资源,且缓存的标识符不会发生更新,浏览器会继续使用旧的缓存标识符来检查资源在下一次请求时是否仍然是最新的。
若验证结果为已更新,服务器会返回最新资源200。
3.2.1 Last-Modified / If-Modified-Since
当我们第一次发出请求时,Last-Modified 由服务器返回,通知客户端,该资源的最后修改时间。
当我们再次请求该资源时,If-Modified-Since 由客户端发送,这个值就代表了 Last-Modified的值。
服务器收到请求后,将 If-Modified-Since 的值与被请求资源的最后修改时间进行比对。若资源的最后修改时间大于 If-Modified-Since 的值,说明资源被修改过,则返回最新资源以及状态码 200。
若资源的最后修改时间小于或等于 If-Modified-Since 的值,说明资源无修改,则返回状态码304,通知客户端继续使用缓存资源。
例子:
//第二次发出请求时,请求头内容
If-Modified-Since: Mon, 23 Jul 2018 08:29:29 GMT
//第二次发出请求时,响应头内容
Last-Modified: Mon, 23 Jul 2018 08:29:29 GMT
//此时,状态码应该为:
Status Code: 304 Not Modified
3.2.1.1 Last-Modified的精确度限制
Last-Modified 标注的最后修改只能精确到秒级。
如果某些文件在1秒钟以内被多次修改的话,它将不能准确标注文件的修改时间。
同时也要考虑到,一些文件也许会周期性的更改,但是它的内容并不改变,仅仅改变的修改时间。
以上这些情况用Last-Modified/If-Modified-Since就不是很合适了。
3.2.2 ETag / If-None-Match
ETag是资源的唯一标识符,它的出现解决了Last-Modified精确度的限制。
当我们第一次发出请求时,ETag 由服务器返回,其值为该资源的标签。
当我们再次请求该资源时,If-None-Match 由客户端发送,它代表了ETag 的值。服务器收到请求后,将 If-None-Match 的值与被请求资源的标签进行比对。
若资源的标签不等于 If-None-Match 的值,说明资源被修改过,则返回状态码 200 以及最新资源。
若资源的标签等于 If-None-Match 的值,说明资源无修改,则返回状态码 304,通知客户端继续使用缓存资源。
3.2.3 ETag / If-None-Match优先级高于Last-Modified / If-Modified-Since
例子:
//第二次发出请求时,请求头内容
If-None-Match: W/"5ce-164c641f628"
//第二次发出请求时,响应头内容
ETag: W/"5ce-164c641f628"
//此时,状态码应该为:
Status Code: 304 Not Modified
Tips:大厂一般都不怎么用Etag。
因为大厂多使用负载分担的方式来调度HTTP请求。因此,同一个客户端对同一个页面的多次请求,很可能被分配到不同的服务器来响应,而根据ETag的计算原理,不同的服务器,有可能在资源内容没有变化的情况下,计算出不一样的Etag,而使得缓存失效。
3.2.4 为什么先验证Etag再验证last-modified
1. 精确度
ETag精确度更高。Etag通常是基于资源内容的唯一标识(如资源内容的散列值),这意味着只要内容发生变化,ETag就会不同。相比之下,Last-Modified只包含日期和时间信息,它只能表达资源何时被修改,而不是修改的具体内容。因此,ETag提供了比Last-Modified更精确的资源比较方法。
2. 效率
当资源内容经常更新但修改时间不经常变时,使用ETag可以更有效地处理协商缓存。如果ETag不匹配,说明资源一定发生了变化,浏览器需要重新下载资源。如果ETag匹配,说明资源没有变化,此时再检查Last-Modified日期以确保没有其他更新(虽然这种情况很少见)。
3. 兼容性和灵活性
虽然ETag的精确度更高,但在某些情况下(如跨域资源共享CORS限制),服务器可能只支持Last-Modified。为了保证协商缓存能够在不同环境中工作,浏览器首先检查ETag,如果服务器不支持ETag,浏览器将回退到Last-Modified。
4. 条件请求
HTTP协议规定了三种条件请求头部:If-None-Match(基于ETag),If-Modified-Since(基于Last-Modified),和If-Unmodified-Since。浏览器通常使用If-None-Match和If-Modified-Since来发起条件性GET请求,而ETag由于其更高的精确度和灵活性,通常是首选的条件请求字段。
综上所述,浏览器先使用ETag来判断资源是否变化是因为ETag提供了更为精细的资源比较,能够更高效地处理资源的更新情况。
如果ETag检查失败,浏览器再使用Last-Modified作为第二道防线,进一步确认资源是否真的发生了变化。
3.2.5 衍生出来的问题
如果该资源文件被强制缓存起来了,并且缓存时间还未过期时,如何判断该文件是否已经被修改了呢?也就是此时的文件是否是最新的我们不得而知。在HTTP规范里面并没有解决这个问题的方案。
答:通过不缓存html,为静态文件添加MD5或者hash标识,解决浏览器无法跳过缓存过期时间主动感知文件变化的问题。因为加上了hash标识,使得每次请求时的路径名不完全相等,也就不能够直接拿本地缓存了,进而去找协商缓存,从而访问服务器。当然加hash标识这一步,现在可以用打包工具来实现。
4. 如何缓存文件并寻找对应缓存文件
4.1 缓存资源是如何缓存在内存或硬盘的
(1)首先,缓存数据是一定会存储在硬盘。
(2)其次,部分资源在浏览器加载后(如打开浏览器一个tab),从硬盘加载内存中来,一般是比较小一点的非异步加载文件。
(3)当需要再次加载该资源时(如地址栏回车刷新页面),会优先取内存缓存,内存缓存不存在才会继续取硬盘缓存。
(4)当浏览器关闭后,内存缓存会清空。
例如:
第一次加载页面,大部分资源都是从硬盘中加载,即from disk cache
刷新该页面,不少资源变成了从内存加载,即from memory cache
4.2 如何寻找已缓存的文件
(1)先去内存看,如果有,直接加载。(内存缓存直接缓存在浏览器中,所以浏览器关闭时,内存缓存也不在了)
(2)如果内存没有,择取硬盘获取,如果有直接加载。(硬盘缓存通常存储在本地硬盘的一个文件系统中,读取速度比内存缓存慢,但可以长期保存资源。)
(3)如果硬盘也没有,那么就进行网络请求
(4)加载到的资源缓存到硬盘和内存
以图片为例:
首次访问图片 ---> 200返回图片资源 ---> 退出浏览器
再次进入浏览器访问图片 ----> 200(from disk cache)---> 刷新 ---> 200(from memory cache)
5. 内存缓存和硬盘缓存
5.1 200 from memory cache
不访问服务器,一般已经加载过该资源并且缓存在了内存当中,直接从内存中读取缓存。
浏览器关闭后(准确来讲应该是页签(tab)),数据将不存在(资源被释放掉了。
再次打开相同的页面时,不会出现 from Memory Cache。
这种方式只能缓存派生资源。
5.2 200 from disk cache
不访问服务器,已经在之前的某个时间加载过该资源,直接从硬盘中读取缓存。
关闭浏览器后,数据仍然存在,此资源不会随着该页面的关闭而释放掉。
再次打开相同的页面时,出现from Disk Cache。
这种方式也只能缓存派生资源。
5.3 304 Not Modified
访问服务器,发现数据没有更新,服务器返回此状态码,然后从缓存中读取数据。
5.4 强缓存返回的状态码200
强缓存时,不管是from memory cache还是from disk cache返回的状态码都是200。
原因在于HTTP协议的设计。
即使资源是从客户端缓存中直接提供的,而不是从服务器重新获取的,HTTP响应仍然是由原始服务器生成的。服务器在响应中包含了足够的元数据,表明资源是新鲜的,即它的状态没有改变,仍然是最新的。
5.5 哪些文件会放在内存而非硬盘
事实上,这个划分规则一直以来是没有定论的。
但从日常开发中观察的结果可以发现,Base64格式的图片,几乎永远可以被塞进memory cache,这可以视作浏览器为节省渲染开销的“自保行为”。
另外,体积不大的JS、CSS文件,也有较大地被写入内存几率。
相比之下,较大的JS、CSS文件就没有这个待遇了,内存资源是有限的,它们往往被直接甩进磁盘(Disk Cache)。
6. must-revalidate是什么
must-revalidate,通常出现在cache-control的指令值中。
例如:
Cache-Control: must-revalidate
revalidate,可以理解成“再次校验”的意思:即再次校验缓存是不是真的过期了,如果真过期了的话返回 200,否则返回 304。
must-revalidate指令,即是强制要求浏览器在一个缓存过期之后,不能直接使用这个过期缓存,必须校验之后才能使用。
我们前面介绍浏览器的缓存机制,会在缓存没有过期时会直接使用缓存资源,而在缓存资源过期时去服务端校验资源并根据结果决定返回什么。
也就是说,只要缓存过期,浏览器就会自动去校验,自动“触发must-revalidate行为” 。
这么听起来must-revalidate的作用似乎有点多余?似乎没有must-revalidate浏览器也会去校验过期的资源?
原来 must-revalidate生效的场景还有一个大前提,那就是 HTTP 规范是允许客户端在某些特殊情况下直接使用过期缓存的,比如校验请求发送失败的时候,还比如有配置一些特殊指令(stale-while-revalidate、stale-if-error等)的时候。
must-revalidate在缓存服务器上有一点点作用,但比较小众;在浏览器端几乎没有任何作用。绝大多数情况,人们都是把它误用为 no-cache了。或者是完全没细研究,直接把max-age=0, no-cache, no-store, must-revalidate一坨都塞进去了,反正能 work 就不管了。
另外,通过must-revalidate的字面意思我们可以明白,这个“必须再次校验”是针对过期资源的,而浏览器对于过期资源本身就会触发这样的校验操作。所以must-revalidate的作用聊胜于无。
7. 页面加载或刷新时浏览器做了什么
当用户 Ctrl + F5 强制刷新网页时,浏览器直接从服务器加载,跳过强缓存和协商缓存。
当用户仅仅敲击 F5 刷新网页时,跳过强缓存,但是仍然会进行协商缓存过程。