前言
作为一个前端开发者,每天都在和浏览器打交道,对于浏览器缓存,相信都不会陌生,同时它也是我们日常开发中存在的一个非常重要的优化手段,无论在节省带宽、提高加载和渲染速度、减少网络阻塞,以及提高用户体验上,都有重要的作用。
为什么需要浏览器缓存?
对于浏览器缓存,主要针对的是前端的静态资源,最好的效果是,在发起请求后,拉取相应的静态资源,并保存在本地。如果服务器的静态资源没有更新,那么在下次请求的时候,直接从本地读取资源即可;如果服务器的静态资源已经更新,那么我们再次请求的时候,就要去服务器拉取新的资源,并保存到本地。这样就大大减少了请求次数,提高网站性能。这就用到浏览器的缓存策略了。
总之使用浏览器缓存,有以下优点:
- 加快客户端网页加载速度,减少用户等待时间,提升用户体验,直接从内存或磁盘中取缓存数据肯定是比从服务器请求更快的。
- 减少了冗余的数据传输。
- 减少服务器的负担,大大提升了网站性能。
DNS缓存
这是我们输入网址后,最开始的一个缓存;通常我们输入一个网址,它包含了域名和端口可以指定唯一的IP地址,然后建立连接进行通信,而域名查找IP地址的过程就是DNS解析。
www.baidu.com (域名) - DNS解析 -> 180.76.76.76 (IP地址)
这个过程会对网络请求带来一定的损耗,所以浏览器在第一次获取到IP地址后,会将其缓存起来。下次相同域名再次发起请求时,浏览器会先查找本地缓存,如果缓存有效,则会直接返回该IP地址,否则会继续开始寻址之旅。 关于寻址过程请看我之前的文章 浏览器从输入URL到页面渲染加载的过程(浏览器知识体系整理)
缓存读写顺序
备注:
memory cache =》浏览器内存缓存
disk cache =》 硬盘缓存
- 先在浏览器内存中查找,如果有,直接加载。
- 如果内存中不存在,则在硬盘中查找,如果有直接加载。
- 如果硬盘中也没有,向服务器发起网络请求。
- 请求获取的资源缓存到硬盘和内存。
下面将从 缓存位置 和 缓存策略 两个角度介绍浏览器缓存。
缓存位置
memory cache(浏览器内存缓存)
- 顾名思义,就是将资源缓存到内存中,等待下次访问时不需要重新下载资源,而直接从内存中获取。
- 相比于 disk cache 它的特点是读取速度快,但容量小,且时效性短;不受开发者控制,也不受HTTP协议头的约束。一旦浏览器
tab页关闭,memory cache就将被清空,再次重新打开相同页面时不再出现from memory cache的情况。 - memory cache 会自动缓存所有资源嘛?答案肯定是
否定的,当 HTTP 头设置了Cache-Control: no-store的时候或者浏览器开发者工具 network 设置了Disabled cache禁用缓存后,就无法把资源存入内存了,其实也无法存入硬盘。当从 memory cache 中查找缓存的时候,不仅仅会去匹配资源的 URL,还会看其Content-type是否相同。
disk cache(磁盘缓存)
顾名思义,就是将资源缓存到磁盘中,等待下次访问时不需要重新下载资源,而直接从磁盘中获取。(磁盘==硬盘,可以互换使用的术语)
根据 HTTP 响应头的各类字段进行判定资源的缓存规则,比如是否可以缓存,什么时候过期,过期之后是否需要重新发起请求呢?相比于 memory cache 的 disk cache 拥有存储空间时间长等优点。
| - | memory cache(内存缓存) | disk cache(磁盘缓存) |
|---|---|---|
| 相同点 | 都是浏览器的本地缓存机制 | 都是浏览器的本地缓存机制 |
| 不相同点 | 退出进程时会被清除 | 退出进程时不会被清除 |
| 存储资源 | 例如图像、样式表、脚本等保存在内存中 | 一般保存非脚本,如css等 |
注意:
- 因为CSS文件加载一次就可渲染出来,我们不会频繁读取它,所以它不适合缓存到内存中,但是js之类的脚本却随时可能会执行,如果脚本在磁盘当中,我们在执行脚本的时候需要从磁盘取到内存中来,这样IO开销就很大了,有可能导致浏览器失去响应。
- 无论是内存缓存还是硬盘缓存,都存在强缓存和协商缓存的概念。具体使用哪种缓存策略取决于服务器返回的响应头信息和浏览器的缓存配置。
缓存策略 - 强缓存和协商缓存
采用网络图片进行学习。
根据 HTTP header 的字段将缓存分为两个部分,分别是强缓存和协商缓存。
- 强缓存:使用强缓存策略时,如果缓存资源在
过期时间内,是的话直接从本地缓存中读取资源,不与服务器进行通信。 - 协商缓存:如果强缓存失效后,客户端将向服务器发出请求,进行协商缓存。浏览器携带上一次请求返回的响应头中的 缓存标识 向服务器发起请求(如ETag、Last-Modified等),由服务器判断资源是否更新。如果
资源没有更新,则返回状态码304Not Modified,告诉浏览器可以使用本地缓存;否则返回新的资源内容。强缓存优先级高于协商缓存,但是协商缓存可以更加灵活地控制缓存的有效性。
强缓存的字段有:Expires 和 Cache-Control。协商缓存的字段有:Last-Modified 和 ETag。
- 当
Expires和Cache-Control都被设置的时候,浏览器会优先考虑后者。 - 当
Last-Modified和ETag都被设置的时候,浏览器会优先考虑后者。
1)强缓存
当客户端发出一个请求到服务器,服务器希望你把资源缓存起来,于是在响应头中加入了这些内容:
Cache-Control: max-age=3600 // 我希望你把这个资源缓存起来,缓存时间是3600秒(1小时)
Expires: Mon Oct 17 2023 16:10:32 GMT // 到达指定时间过期
Date: Mon Oct 16 2023 13:30:30 GMT
Etag:W/"121-171ca289ebf",// (后面协商缓存内容)这个资源的编号是W/"121-171ca289ebf"
Last-Modified: Mon Oct 16 2023 09:20:10 GMT ,// (后面协商缓存内容)这个资源的上一次修改时间
Cache-Control和 Expires分别是HTTP/1.1 和 HTTP/1.0的内容,为了兼容 HTTP/1.0 和 HTTP/1.1,实际项目中两个字段我们都会设置。
浏览器收到这个响应之后就会做下面的事情
- 浏览器把这次请求得到的响应体缓存到本地文件中
- 浏览器标记这次请求的请求方法和请求路径
- 浏览器标记这次缓存的时间是3600秒
- 浏览器记录服务器的响应时间是格林威治时间2023-10-16 09:20:10
这一次的记录非常重要,它为以后浏览器要不要去请求服务器提供了依据。
判断缓存是否有效就是通过把 max-age + Date,得到一个过期时间,看看这个过期时间是否大于当前时间,如果是,则表示缓存还没有过期,仍然有效,如果不是,则表示缓存失效。
Expires
Expires 是HTTP/1.0的字段,表示缓存过期时间。Expires 需要在服务端配置(具体配置也根据服务器而定),浏览器会根据该过期日期与客户端时间对比,如果过期时间还没到,则会去缓存中读取该资源,如果已经到期了,则浏览器判断为该资源已经过期需要重新从服务端获取。由于 Expires 是一个绝对时间,所以会局限于客户端时间的准确性,从而可能会出现浏览器判断缓存失效的问题。所以出现了Cache-Control,如下是一个 Expires 示例,是一个日期/时间:
Expires: Mon Oct 17 2023 16:10:32 GMT
到了HTTP/1.0版本,已更改为通过Cache-Control的max-age来记录了。
Cache-Control
Cache-Control 是 http1.1 时出现的响应头信息,主要通过Cache-Control的max-age来记录。下面是几个比较常用的设置值:
max-age:最大缓存时间,它是一个相对时间,值的单位是秒,在该时间内,浏览器不需要向浏览器请求。这个设置解决了 Expires 中由于客户端系统时间不准确而导致缓存失效的问题。no-cache:每次使用前需要向源服务器确认下,防止从缓存中返回过期的资源。no-store:禁止使用缓存,每次都要重新请求数据,不会被缓存到内存和硬盘。public:响应可以被任何对象(客户端、代理服务器等)缓存。private:与public相反,缓存服务器会对特定用户提供资源缓存,对于其他用户发送来的请求,代理服务器不会返回缓存。
Cache-Control 的值是可以混合使用的,比如:
Cache-Control: private, max-age=0, no-cache
当强缓存失效的时候,则会进入到协商缓存阶段。
2)协商缓存
一旦发现强缓存无效,浏览器会发送一个请求到服务器,服务器根据请求header中的部分信息来判断资源是否更新。如果没有更新,返回304重定向,告诉浏览器资源未更新,可继续使用本地的缓存;否则返回 状态码200 和 新的资源内容,浏览器缓存新的内容。
这里的请求头header,就是加入了
If-Modified-Since: Mon Oct 16 2023 09:20:10 GMT 你好,你曾经告诉我,这个资源的上一次修改时间是格林威治时间2023-10-16 09:20:10,请问这个资源在这个时间之后有发生变动吗?
If-None-Match: W/"121-171ca289ebf" 你好,你曾经告诉我,这个资源的编号是W/"121-171ca289ebf,请问这个资源的编号发生变动了吗?
其实响应头和请求头的对应关系就是 Last-Modify/If-Modify-Since 和 ETag/If-None-Match。
之所以要发两个信息,是为了兼容不同的服务器,因为有些服务器只认If-Modified-Since,有些服务器只认If-None-Match,有些服务器两个都认,但是一般来说 If-None-Match 的优先级高于 If-Modified-Since
Last-Modify / If-Modify-Since
浏览器第一次请求一个资源的时候,服务器返回的 header 中会加上 Last-Modify,Last-Modify是一个时间标识该资源的最后修改时间。
当浏览器再次请求该资源时,请求头中会包含 If-Modify-Since,该值为缓存之前返回的 Last-Modify。服务器收到 If-Modify-Since 后,根据资源的最后修改时间判断资源是否更新。
- 资源未更新,返回304重定向,表示资源未更新可以继续使用缓存中的资源。
- 资源更新,返回200状态码,返回新的资源,并进行硬盘和浏览器缓存。
缺点:如果资源更新的速度是小于 1 秒的,那么该字段将失效,因为 Last-Modified 时间是精确到秒的。所以有了 ETag。
ETag / If-None-Match
与 Last-Modify/If-Modify-Since 不同的是,Etag/If-None-Match 返回的是一个校验码。ETag 可以保证每一个资源是唯一的,资源变化都会导致 ETag 变化。服务器根据浏览器上发送的 If-None-Match 值来判断是否缓存。
与 Last-Modified 不一样的是,当服务器返回 304 Not Modified 的响应时,由于 ETag 重新生成过,response header 中还会把这个 ETag 返回,即使这个 ETag 跟之前的没有变化。
浏览器的行为
- 浏览器地址栏输入 URL 后回车: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
- 普通刷新 (F5 键):因为
tab页并没有关闭,因此memory cache是可用的,会被优先使用(如果匹配的话),其次才是disk cache。 - 强制刷新 ( Ctrl + F5 键):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache)。服务器直接返回 200 和最新内容。
- 当在开发者工具
Network面板下设置了Disabled cache禁用缓存后,浏览器将不会从memory cache或者disk 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的请求无法被缓存;
知识拓展
通过前端代码是不能控制缓存的,缓存是后端或者部署的同学来控制的,但是前端同学应该知道那些内容要被缓存,和后端或者部署的同学配合来打! 本文参考