浏览器缓存机制
一 什么是浏览器缓存
浏览器缓存是将文件保存在客户端。在同一个回话过程中检查缓存的副本是否足够新,在后退网页时,访问过的资源可以从浏览器缓存中拿出来使用。
它有很多作用:
1.减少网络带宽消耗
无论对于网站运营者或者用户,带宽都代表着金钱,过多的带宽消耗,只会便宜了网络运营商。当缓存副本被使用时,只会产生极小的网络流量,可以有效的降低运营成本。
2.降低服务器压力
给网络资源设定有效期之后,用户可以重复使用本地的缓存,减少对源服务器的请求,间接降低服务器的压力。同时,搜索引擎的爬虫机器人也能根据过期机制降低爬取的频率,也能有效降低服务器的压力。
3.减少网络延迟,加快页面打开速度
带宽对于个人网站运营者来说是十分重要,而对于大型的互联网公司来说,可能有时因为钱多而真的不在乎。那缓存还有作用吗?答案是肯定的,对于最终用户,缓存的使用能够明显加快页面打开速度,达到更好的体验。
二 浏览器缓存的基本组成
浏览器缓存主要分为强缓存和协商缓存,这个过程如下:
1.浏览器在加载资源时,先根据这个资源的http header判断它是否命中强缓存,如果命中,浏览器直接从自己的缓存中读取资源,不发送请求到服务器;
2.如果没有命中强缓存,浏览器会发送一个请求到服务器,服务器根据资源的http header验证这个资源是否命中协商缓存,如果命中,服务器会将这个请求返回,但是不会返回这个资源的数据,而是告诉客户端可以直接从缓存中加载这个资源,于是浏览器就又会从自己的缓存中去加载这个资源;
3.当协商缓存也没有命中的时候,浏览器就直接从服务器加载数据
强缓存和协商缓存的共同点是:如果命中,都是从客户端中加载资源
它们的区别是:强缓存不发请求到服务器,协商缓存会发送请求到服务器
三 强缓存
当浏览器对某个资源的请求命中了强缓存时,返回的HTTP状态为200,Size为from cache,很多大型网站首页就有很多静态资源配置了强缓存,在Chrome开发者模式里就可以看到:
强缓存可以通过Expires或者Cache-Control这两个http response header实现,它们都用来表示资源在客户端缓存的有效期。
Expires
Expires是web服务器响应消息头(response header)字段,在响应http请求时告诉浏览器在过期时间内请求资源可以直接从浏览器缓存读取数据。它描述的是一个绝对时间,由服务器返回,用GMT格式的字符串表示。
它的缓存原理如下:
1.浏览器第一次跟服务器请求一个资源时,服务器在返回这个资源的同时,在response的header加上Expires,如图:
2.浏览器接收到这个资源后,会把这个资源连同所有的response header一起缓存下来(所以缓存命中的请求返回的header并不是来自服务器,而是来自之前缓存的header)
3.浏览器再次请求这个资源时,先从缓存中找,找到这个资源后,将Expires与当前的请求时间比较,如果请求时间在Expires指定的时间之前则命中缓存,否则无法命中缓存;
4.如果没有命中缓存,浏览器就直接从服务器加载资源,这时的Expires在重新加载后会被更新。
缺点:
1.Expires由于是服务器返回的一个绝对时间,在服务器与客户端时间相差较大时,缓存管理容易出现问题。比如说随意更改下客户端缓存时间,就能影响缓存命中的结果。所以HTTP1.1提出了Cache-Control来管理缓存,Cache-Control有很多的配置和更好的性能,应用广泛。
2.Expires是HTTP1.0的一个表示资源过期时间的header,在目前广泛应用的HTTP1.1版本中,它的作用几乎可以忽略。
Cache-Control
Cache-Control也是指明当前资源的有效期,相比Expires,它的选择更多,设置更精细,如果同时设置的话,Cache-Control的优先级高于Expires。
Cache-Control是一个通用首部字段,这意味着它能在请求报文和响应报文中使用。在RFC中规范了Cache-Control的格式:
Cache-Control:"cache-directive"
作为请求首部时,cache-directive的可选值包括:
字段名称 | 说明 |
---|---|
no-cache | 告知(代理)服务器不直接使用缓存,要求向员服务器发起请求 |
no-store | 所有内容都不会被保存到缓存或Internet临时文件中 |
max-age=delta-seconds | 告知服务器 客户端希望接收一个存活时间不大于delta-seconds秒的资源 |
max-stale[=delta-seconds] | 告知(代理)服务器 客户端愿意接收一个超过缓存时间delta-seconds秒的资源,若没有定义具体时间,则默认为任意超出的时间 |
min-fresh=delta-seconds | 告知(代理)服务器 客户端希望接收一个在小于delta-seconds秒内更新过的资源 |
no-transform | 告知(代理)服务器 客户端希望获取实体数据没有被转换(比如压缩)过的资源 |
only-if-cached | 告知(代理)服务器 客户端希望获取缓存的内容(若有),而不用向原服务器发送请求 |
cache-extension | 自定义拓展值,若服务器不识别该值则直接忽略 |
作为请求首部时,cache-directive的可选值包括:
字段名称 | 说明 |
---|---|
no-cache | 告知(代理)服务器不直接使用缓存,要求向员服务器发起请求 |
no-store | 所有内容都不会被保存到缓存或Internet临时文件中 |
max-age=delta-seconds | 告知服务器 客户端希望接收一个存活时间不大于delta-seconds秒的资源 |
max-stale[=delta-seconds] | 告知(代理)服务器 客户端愿意接收一个超过缓存时间delta-seconds秒的资源,若没有定义具体时间,则默认为任意超出的时间 |
min-fresh=delta-seconds | 告知(代理)服务器 客户端希望接收一个在小于delta-seconds秒内更新过的资源 |
no-transform | 告知(代理)服务器 客户端希望获取实体数据没有被转换(比如压缩)过的资源 |
only-if-cached | 告知(代理)服务器 客户端希望获取缓存的内容(若有),而不用向原服务器发送请求 |
cache-extension | 自定义拓展值,若服务器不识别该值则直接忽略 |
另外,Cache-Control允许多种自由组合,如:
Cache-Control: max-age=3600, must-revalidate
它意味着该资源是从原服务器上取得的,且其缓存(新鲜度)的有效时间为一小时,在后续一小时内,用户重新访问该资源则无须发送请求。
当然这种组合的方式也会有些限制,比如 no-cache 就不能和 max-age、min-fresh、max-stale 一起搭配使用。
管理强缓存
实际应用中我们可以通过以下两种方式设置强缓存和不需要强缓存:
1.通过代码的方式,在web服务器返回的响应中添加Expires和Cache-Control Header;
//1.设置强缓存 java.util.Date date = new java.util.Date(); response.setDateHeader("Expires",date.getTime()+20000); //Expires:过时期限值 response.setHeader("Cache-Control", "public"); //Cache-Control来控制页面的缓存与否,public:浏览器和缓存服务器都可以缓存页面信息; response.setHeader("Pragma", "Pragma"); //Pragma:设置页面是否缓存,为Pragma则缓存,no-cache则不缓存 //2.不启用强缓存 response.setHeader( "Pragma", "no-cache" ); response.setDateHeader("Expires", 0); response.addHeader( "Cache-Control", "no-cache" );//浏览器和缓存服务器都不应该缓存页面信息
2.通过配置web服务器的方式,让web服务器在响应资源的时候统一添加Expires和Cache-Control
tomcat还提供了一个ExpiresFilter专门来配置强缓存,具体的配置方法可以参考 tomcat官方文档
nginx和apache作为专业的web服务器,都有专门的配置文件可以配置expires和Cache-Control,可以搜索"apache expires Cache-Control"能找到不少这方面的文章。
由于在开发的时候不会专门去设置强缓存,而浏览器又默认会缓存图片,css和js等静态文件,所以开发环境下经常会因为强缓存导致资源没有及时更新而看不到最新的效果,解决这个问题的方法有很多,常用的方法有以下几种:
(1)直接Ctrl+F5,这个办法能解决页面直接引用的资源更新的问题(跳过强缓存和协商缓存);直接F5刷新网页会跳过强缓存,但是会检查协商缓存
(2)使用浏览器的隐私开发模式
(3)如果使用的是Chrome,可以F12在network那里把缓存给禁调(这个方法非常有效)
(4)如果你用的是grunt/gulp这种工具,通过它们的插件如grunt-contrib-connect来启动一个静态服务器,则完全不用担心开发阶段的资源更新问题,因为这个静态服务器下的所有资源返回的response header中,cache-control始终被设置为不缓存:
强缓存的应用
强缓存是前端性能优化最有力的工具,没有之一,对于有大量静态资源的网页,一定要利用强缓存提高响应速度。通常的做法是为这些静态资源配置一个超长时间的Expires或Cache-Control,这样用户在访问网页时,只会在第一次加载事从服务器请求静态资源,其他时候只要缓存没有失效并且用户没有强制刷新的条件下都会从自己的缓存中加载。比如京东首页缓存的资源,它的缓存时间已经到了2026年:
但是这么做又会带来一个新的问题,就是发布时资源更新的问题,比如一个图片在用户访问第一个版本时就已经缓存到了用户的电脑上,当网站发布新版本时,替换了这个图片,如果他不清掉或禁用缓存或者强制刷新,那他就看不到最新的图片效果。(以后解决)
强缓存还有一点需要注意,通常都是针对静态资源使用,而动态资源一定要慎重使用,一旦这些动态资源也被缓存,当这些html更新后,可能就没有机制能够通知浏览器这些html有更新。尤其是在前后端分离的应用里,页面都是纯html页面,每个访问地址可能都是直接访问html页面,这些页面通常不加强缓存,以保证浏览器访问这些页面时时候请求服务器最新的资源。
四 协商缓存
当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中。如果协商缓存命中,请求响应返回的http状态就为304并且会显示一个Not Modified的字符串,比如说你打开京东的首页,按F12打开开发者模式,再按F5刷新页面,查看network,可以看到不少请求就是中了协商缓存的:
协商缓存主要是通过[Last-Modified,If-Modified-Since]和[ETag、If-None-Match]这两对header来管理的。
[Last-Modified,If-Modified-Since]控制缓存的原理
1.浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在response的header加上Last-Modified的header,这个header表示这个资源在服务器上的最后修改时间:
2.当浏览器再次跟服务器请求这个资源时,在request的header上加上If-Modified-Since的header,这个header的值就是上一次请求时返回的Last-Modified的值:
3.服务器再次收到资源请求时,根据浏览器传过来的If-Modified-Since和资源在服务器上的最后修改时间是否一致,如果一致则返回304 Not Modified;如果有变化,就正常返回资源内容。当服务器返回304 Not Modified的响应时,response header中不会再添加Last-Modified的header,因为资源没有变化。
4.浏览器收到304的响应后,就会从缓存中加载资源。
5.如果协商缓存没有命中,浏览器直接从服务器加载资源,Last-Modified这个header就会在重新加载的时候被更新,下次请求时,If-Modified-Since就会启用上次返回的Last-Modified值。
Last-Modified,If-Modified-Since都是根据服务器时间返回的header,一般来说,在没有调整服务器时间和篡改客户端缓存的情况下,这两个header配合起来管理协商缓存是非常可靠的。但是也可能会出现以下几种情况:
1.服务器资源其实有变化,但Last-Modified没有变化的情况,这种问题不容易定位,一旦出现很影响协商缓存的可靠性。
2.Last-Modified只能精确到秒,如果一个文件在1秒内被多次修改的话,它将不能准确标注文件的修改时间。
3.对于某些会定期生成的文件,它们的内容并没有改变,但是Last-Modified变了,导致整个文件无法使用缓存。
这时就需要另一对header来管理协商缓存,即[ETag, If-None-Match],这对header与[Last-Modified,If-Modified-Since]的主要区别在于:
浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,会在response的header加上ETag的header,这个header是服务器根据当前请求的资源生成唯一的一个标志,这个标志是一个字符串,如果这个资源以后发生变化,那么这个字符串就会改变:
浏览器再次请求的时候就会在request的header上加上If-None-Match的header,这个header就是上一次请求时返回的ETag。接下来的过程跟上面那对header是一样的。
协商缓存的管理
1.协商缓存与强缓存不一样的是,协商缓存一定后请求服务器资源,资源是否更新,服务器肯定会知道。大部分的web服务器都默认开启协商缓存,而且是同时开启[Last-Modified,If-Modified-Since]和[ETag、If-None-Match],这是为了为了防止Last-Modified不可靠的情况。
2.分布式系统还需要注意以下情况:
(1)分布式系统里多台机器间文件的Last-Modified必须保持一致,以免出现负载均衡到不同机器导致对比失败;
(2)分布式系统尽量关闭掉ETag,因为每台机器生成的ETag都会不一样。
3.一般情况下,强缓存和协商缓存是同时存在的,如果不启用强缓存,协商缓存根本没有意义。如图:
有些请求无法被缓存:
1.HTTP消息头中包含Cache-Control:no-cache,pragma:no-cache,或Cache-Control:max-age=0等告诉浏览器不用缓存的请求
2.需要根据cookie,认证信息等决定输入内容的动态请求时不能被缓存的
3.经过HTTPS安全加密的请求
4.POST请求
5.HTTP响应头中不包含Last-Modified/Etag,也不包含Cache-Control/Expires的请求无法被缓存