之前在工作中遇到过一个很让人费解的生产问题,每次前端项目发版后总有个别用户(手机型号:华为p30、mate 20 pro)反馈页面白屏,但我和同事的手机访问一直都很正常。每次定位问题都要花很长时间,而且也没能找到问题,然后第二天用户反馈又好了。最后做了一些猜测,可能是浏览器缓存的问题。然后就按这个方向找解决办法,最后是在nginx加了对资源的缓存控制后,再也没有遇到这种问题。所以就收集了相关知识写下了这篇文章,希望可以帮忙更多的朋友。
一、概述
HTTP缓存可以说是HTTP性能优化中最简单高效的一种优化方式。缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。当web缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载。
一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,节省网络流量,并且由于缓存文件可以重复利用,降低网络负荷,提高客户端响应。
二、缓存的优缺点
优点:
- 减少网络流量消耗:无论对于网站运营者或者用户,网络流量都代表着金钱。当Web缓存副本被使用时,只会产生极小的网络流量,可以有效的降低运营成本。
- 降低服务器压力:给网络资源设定有效期之后,用户可以重复使用本地的缓存,减少对源服务器的请求,间接降低服务器的压力。同时,搜索引擎的爬虫机器人也能根据过期机制降低爬取的频率,也能有效降低服务器的压力。
- 减少网络延迟,加快页面打开速度:对于用户来说,缓存的使用能够明显加快页面打开速度,达到更好的体验。
缺点:
- 不能及时让用户获取到最新内容
- 甚至如果缓存设置不合理还会导致页面报错(主要是因为未找到资源导致)
我们需要避开缺点,让优点最大化。这就需要对我们网站资源的缓存进行合理控制才行。下面会讲到具体如何控制。
三、http网络协议中与缓存相关的属性
在讲几种缓存之前,我们先讲一下http网络协议中和缓存有关的首部信息。
3.1、 【响应报文】首部的缓存属性:
属性名 | 值 | 优先级 | http版本 | 说明 |
---|---|---|---|---|
Cache-control | no-store | 高 | 1.1 | 绝对禁止缓存资源(不缓存) |
-- | no-cache | 高 | 1.1 | 浏览器会缓存资源,但每次都会向服务器确认资源是否发生改变(协商缓存) |
-- | max-age = xxx | 高 | 1.1 | 缓存时长(缓存的资源将在 xxx 秒后过期) |
-- | s-maxage = xxx | 高 | 1.1 | 代理服务器缓存时长(cdn缓存时长,优先级高于max-age与expires) |
-- | public | 高 | 1.1 | 客户端和中间缓存服务器均可缓存(允许cdn缓存) |
-- | private | 高 | 1.1 | 仅客户端可以缓存(禁止cdn缓存) |
-- | must-revalidate | 高 | 1.1 | 如果资源过期,则向服务器获取新资源 |
-- | proxy-revalidate | 高 | 1.1 | 要求中间缓存服务器对缓存的响应有效性再进行确认 |
Expires | Date | 低 | 1.0 | 资源过期时间(依赖客户端时间,容易出现偏差) |
Pragma | no-cache | 低 | 1.0 | 用来向后兼容只支持 HTTP/1.0 协议的缓存服务器,它的行为与 Cache-Control: no-cache 一致 |
Etag | string | 高 | 1.1 | 资源的标识,一般为md5或者hash值 |
Last-modified | Date | 低 | 1.0 | 资源上次修改时间 |
3.2、 【请求报文】首部的缓存属性:
属性名 | 值 | 优先级 | http版本 | 说明 |
---|---|---|---|---|
Cache-control | max-age = xxx | 高 | 1.1 | 缓存时长(缓存的资源将在 xxx 秒后过期) |
-- | no-cache | 高 | 1.1 | 浏览器会缓存资源,但每次都会向服务器确认资源是否发生改变 |
Pragma | no-cache | 低 | 1.0 | 用来向后兼容只支持 HTTP/1.0 协议的缓存服务器,它的行为与 Cache-Control: no-cache 一致 |
If-None-Match | string | 高 | 1.1 | 客户端保留的资源标识 |
If-Modified-Since | Date | 低 | 1.0 | 客户端保留的资源上次的修改时间 |
3.3、 缓存相关属性总结
- 以上首部字段中
Cache-Control
和Pragma
是通用首部字段,所以在请求报文和响应报文中都有。本文中我们主要看他在响应报文中的作用。 Cache-Contro
的值比较多,最常用的有no-store
、no-cache
、max-age
。其它几个值用的频率并不高,主要是对代理服务器的缓存控制。Cache-Contro
具体使用如下:
Cache-Control: private, max-age=0, no-cache
- 响应报文首部字段
Etag
和请求报文首部字段If-None-Match
是成对存在的,用来判断服务器资源是否有变更。 - 响应报文首部字段
Last-modified
和请求报文首部字段If-Modified-Since
是成对存在的,是用来判断服务器资源自上次修改后有没有再被修改。
Pragma
虽是通用首部字段,但只用在客户端发送的请求中。客户端会要求所有的中间服务器不返回缓存的资源。Pragma
是http1.0版本中的首部字段,现在已经被Cache-Contro
所替代。
四、缓存分类
了解完上面的首部字段后,就可以学习几种缓存方式了。
最常见的缓存策略有:强制缓存、协商缓存。 还有两种不太常见的缓存表现形式有:不缓存和启发式缓存。不缓存是直接不缓存,而启发式缓存则算是浏览器自己定义的缓存策略。
4.1、 强制缓存
强制缓存就是在第一次访问服务器取到数据之后,在过期时间之内不会再去重复请求,而是直接读取本地缓存数据库中的信息(from memory or from disk),两种方式根据浏览器的策略随机获取。
当发起请求的时间超过了设定的时间,即表示资源缓存时间到期,会再次发送请求到服务器重新获取资源。而如果发起请求的时间在限定的时间之内,浏览器会直接读取本地缓存数据。
实现这个流程的核心就是如何知道当前时间是否超过了过期时间。强制缓存的过期时间通过第一次访问服务器时返回的响应头获取。在 http 1.0 和 http 1.1 版本中通过不同的响应头字段实现。
在 http 1.0 版本中,强制缓存通过Expires
响应头来实现。 expires 表示未来资源会过期的时间。
在 http 1.1 版本中,强制缓存通过 Cache-Control: max-age=xxx
响应头来实现。
一般来说,为了兼容,两个版本的强制缓存都会被实现。
强制缓存实例
首次访问时
第二次访问时
从上面可以看出,第一次请求从服务器返回数据。第二次请求直接从本地缓存(disk cache)中返回数据,速度快了很多倍。两次状态码都为200。
强制缓存总结
强制缓存只有首次请求才会跟服务器通信,读取缓存资源时不会发出任何请求,资源的 Status 状态码为 200,资源的 Size 为 from memory 或者 from disk ,http 1.1 版本的实现(Cache-Control)优先级会高于 http 1.0 版本的实现(Expires)。
两种本地缓存的对比
存储方式 | memory cache | disk cache |
---|---|---|
存储周期 | 退出进程时数据会被清除 | 退出进程时数据不会被清除 |
存储资源 | 一般脚本、字体、图片会存在内存当中 | 一般非脚本会存在内存当中,如css等 |
4.2、 协商缓存
协商缓存与强制缓存的不同之处在于,协商缓存每次读取数据时都需要跟服务器通信,并且多了缓存标识。
- 在第一次请求服务器时,服务器会返回资源,并且返回一个资源的缓存标识,一起存到浏览器的缓存数据库。
- 当第二次请求资源时,浏览器会首先将缓存标识发送给服务器,服务器拿到标识后判断标识是否匹配,如果不匹配,表示资源有更新,服务器会将新数据和新的缓存标识一起返回到浏览器;如果缓存标识匹配,表示资源没有更新,并且返回 304 状态码,浏览器就读取本地缓存数据。
在 http 协议的 1.0 和 1.1 版本中也有不同的实现方式。
http 1.0
-
第一次请求资源时服务器通过 Last-Modified 来设置响应头的缓存标识,并且把资源最后修改的时间作为值填入,然后将资源返回给浏览器。
-
在第二次请求时,浏览器会将 Last-Modified 的信息放到 If-Modified-Since 请求头去访问服务器。服务器会将 If-Modified-Since 中携带的时间与资源修改的时间匹配,如果时间不一致,服务器会返回新的资源,并且将 Last-Modified 值更新,作为响应头返回给浏览器。如果时间一致,表示资源没有更新,服务器返回 304 状态码,浏览器拿到响应状态码后从本地缓存数据库中读取缓存资源。
这种方式有一个弊端,就是当服务器中的资源增加了一个字符,后来又把这个字符删掉,本身资源文件并没有发生变化,但修改时间发生了变化。当下次请求过来时,服务器也会把这个本来没有变化的资源重新返回给浏览器。
http 1.1
在 http 1.1 版本中,服务器通过 Etag 来设置响应头缓存标识。Etag 的值由服务端生成。
- 在第一次请求时,服务器会将资源和 Etag 一并返回给浏览器,浏览器将两者缓存到本地缓存数据库。
- 在第二次请求时,浏览器会将 Etag 信息放到 If-None-Match 请求头去访问服务器。服务器收到请求后,会将服务器中的文件标识与浏览器发来的标识进行对比,如果不相同,服务器返回更新的资源和新的 Etag ,如果相同,服务器返回 304 状态码,浏览器读取缓存。
协商缓存实例
首次请求
再次请求
具体请求响应头信息
协商缓存总结
协商缓存每次请求都会与服务器交互,第一次是拿数据和标识的过程,第二次开始,就是浏览器询问服务器资源是否有更新的过程。每次请求都会传输数据,如果命中缓存,则资源的 Status 状态码为 304 而不是 200 。同样的,一般来讲为了兼容,两个版本的协商缓存都会被实现,http 1.1 版本的实现优先级会高于 http 1.0 版本的实现。
4.3、不缓存
不缓存就是不管本地缓存是否过期、服务器资源是否有更新,每次都会都从服务器返回资源内容。
使用如下Cache-Control
头信息可实现,值为no-store
而不是no-cache
。
Cache-Control: no-store
4.4、启发式缓存
如果我们不对资源做任何缓存控制,浏览器又会如何控制缓存呢?接下来我们找几个浏览器测试一下看看结果。
测试不明确缓存策略时,第二次访问时效果如下
Chrome浏览器
Firefox浏览器
Microsoft Edge浏览器
经测试结果如上,大部分浏览器都和Chrome浏览器是一样的。而Firefox浏览器和Microsoft Edge浏览器又略有不同。所以如果没有设置缓存策略的话,不同浏览器会使用自己的缓存策略,表现各不同。目前看以Chrome浏览器为代表的这种缓存策略是最合理的(接下来的缓存方案中我们会提到)。
可以看出浏览器的默认缓存策略,有时是协商缓存,有时是强制缓存。那强制缓存有效时间为多少呢?
如果一个可以缓存的请求没有设置Expires和Cache-Control,但是响应头有设置Last-Modified信息,这种情况下浏览器会有一个默认的缓存策略时间:(当前时间 - Last-Modified)*0.1
,这就是启发式缓存。
目前看来,大部分浏览器都已经实现了,但是彼此也略有不同。
注:只有在服务端没有返回明确的缓存策略时才会激活浏览器的启发式缓存策略。
启发式缓存的缺点
考虑一个情况,假设你有一个文件没有设置缓存时间,在一个月前你更新了上个版本。这次发版后,你可能得等到3天后用户才看到新的内容了。如果这个资源还在CDN也缓存了,则问题会更严重。
4.5、http缓存流程图
五、缓存方案(服务器端)
5.1、HTML 文档的元数据(<meta>
)
实例:实现协商缓存
<meta http-equiv="Cache-control" content="no-cache">
<meta http-equiv="Pragma" content="no-cache">
- 优点:前端就可实现缓存控制
- 缺点:不能针对不同资源区分控制
5.2、单页面应用的缓存方案
- HTML(index.html): 协商缓存;
- css、js、图片、字体等:强缓存,文件名带上hash使用(使用contenthash)。
具体需要在nginx中配置各资源的缓存首部字段。具体nginx配置如下:
location /{
root html;
index index.html index.htm;
# 对html文件使用协商缓存
if ($request_filename ~* ^.*?.(html|htm)$) {
add_header Cache-Control no-cache;
add_header Pragma no-cache;
}
# 对css、js、图片、字体、媒体等使用强制缓存(缓存30天)
if ($request_filename ~* \.(css|js|png|jpg|jpeg|gif|gz|svg|mp4|ogg|ogv|webm|htc|xml|woff)$) {
add_header Cache-Control max-age=2592000;
expires 30d;
}
# 也可对特定资源不缓存
if ($request_filename ~* config\.json$) {
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
add_header Pragma no-cache;
}
}
除以上的缓存控制外,最好对部署脚本也做一些优化。每次部署前端代码时,不要删除之前的资源,每次都增量部署。这样子的好处是,即使用户端有缓存数据,依然可以从服务器请求到之前的资源,不会因资源加载失败而出现白屏。 缓存控制+增量部署可以做到前端发布对用户毫无影响。
文件匹配语法规则: location [=|~|~*|^~] /uri/ { … }
=
开头表示精确匹配^~
开头表示uri以某个常规字符串开头,理解为匹配 url路径即可。nginx不对url做编码,因此请求为/static/20%/aa
,可以被规则^~ /static/ /aa
匹配到(注意是空格)。~
开头表示区分大小写的正则匹配~*
开头表示不区分大小写的正则匹配!~
和!~*
分别为区分大小写不匹配及不区分大小写不匹配 的正则/
通用匹配,任何请求都会匹配到。
六、用户操作行为与缓存的关系(客户端)
6.1、刷新对于强缓存和协商缓存的影响
- 普通刷新 – 当按下F5或者点击刷新按钮来刷新页面的时候,浏览器将绕过本地缓存来发送请求到服务器。 此时, 强制缓存失效,协商缓存是有效的。
- 强制刷新 – 当按下ctrl+F5来刷新页面的时候, 浏览器将绕过各种缓存(本地缓存和协商缓存), 直接让服务器返回最新的资源。强制缓存和协商缓存都失效。
- 回车或转向 – 当在地址栏上输入回车或者按下跳转按钮的时候, 所有缓存都生效。
除以上操作之后,用户也可通过浏览器设置来清除浏览器缓存。快捷键是:Shift+Ctrl+Delete。注意不要删除Cookie和浏览记录哦,不然之前保存的一些网站的账号密码和浏览记录就没有了哦。
6.2、作为开发人员还可以这样清除缓存
1、PC端可以使用浏览器自带的开发者工具禁用缓存。
按F12打开浏览器开发者工具,在控制台Network面板中,勾选Disable cache
后就会禁用缓存。此时强制缓存和协商缓存都不生效。
2、移动端可使用“URL中拼接时间戳的方法”避开本地缓存。但是要注意拼接在#
之前。
URL拼接时间戳的方法避开缓存很多开发人员基本都会,但是又有很多人并没有正确使用。因为现在单页面应用增加,而很多单页面应用路由使用的是Hash模式,也就是链接中带
#
。如果时间戳加在#
后面,并不会再次发起http请求,所以也就依然会有缓存。
URL中的hash(#号)说明
#
代表网页中的一个位置(锚点)。其右面的字符,就是该位置上的标识符。- 改变
#
会改变浏览器的访问历史,所以现在#
也被用在一些前端框架的路由中。 #
是用来指导浏览器动作的,对服务器端完全无用。所以,HTTP请求中不包括#
。- 改变
#
不触发网页重新加载。
更多关于hash(#号)的内容请点击查看URL中的hash(#号)详解
URL拼接示例
// 原链接
https://www.abc.com/#/index
// 错误的拼接方式
https://www.abc.com/#/index?t=1641805199020
// 正式的拼接方式
https://www.abc.com/?t=1641805199020#/index