[TOC] 简单来讲,浏览器缓存其实就是浏览器保存通过HTTP请求获取的所有资源,是浏览器将网络资源存储在本地的一种行为。
不使用缓存的话:
- 浪费用户流量;
- 浪费服务器资源,服务器要读磁盘文件,然后发送文件到浏览器;
- 浏览器要等待js下载并且执行后才能渲染页面,影响用户体验。
浏览器缓存
浏览器缓存分为强缓存和协商缓存:
- 浏览器在加载资源时,先根据这个请求资源的http header(expires和cahe-control)判断该资源是否命中强缓存,如果命中,浏览器则直接从缓存中读取该资源,不会发请求到服务器。
- 当强缓存没有命中的时候,浏览器一定会发送一个请求到服务器,服务端依据该资源的另外一些http header(Last-Modify/If-Modify-Since和ETag/If-None-Match)验证这个资源是否命中协商缓存,如果命中,服务器会将这个请求返回,但是不会返回这个资源的数据,而是告诉浏览器可以直接从缓存中加载这个资源,于是浏览器就又会从缓存中加载这个资源。
- 当协商缓存也没有命中的时候,浏览器直接从服务器加载对应的资源数据。
缓存好处:
- 减少了不必要数据的传输,节约带宽,节省了网费;
- 减轻了服务器的压力,减少网络延迟,提高了网站的性能;
- 加快了客户端加载页面的速度,提高了用户体验。
强缓存与协商缓存的异同
共同点:如果命中,都是从浏览器缓存中加载资源,而不是从服务器加载资源数据**; 区别:
- 强缓存不发请求到服务器,协商缓存会发请求到服务器。
- 强缓存返回的HTTP状态码为200,协商缓存返回的是304。
缓存资源存储
- memory cache:顾名思义,就是将资源缓存到内存中,等待下次访问时不需要重新下载资源,而直接从内存中获取。Webkit早已支持memoryCache。
- disk cache:顾名思义,就是将资源缓存到磁盘中,等待下次访问时不需要重新下载资源,而直接从磁盘中获取。
三级缓存原理 (缓存优先级)
- 先在内存(memory cache)中查找,如果有,直接加载。
- 如果内存中(memory cache)不存在,则在硬盘中(disk cache)查找,如果有,直接加载。
- 如果硬盘中(disk cache)也没有,那么就进行网络请求。
- 请求获取的资源缓存到硬盘和内存中。
缓存规则
对于浏览器端的缓存来说,这些规则是在HTTP协议头和HTML页面的Meta标签中定义的。他们分别从新鲜度和校验值两个维度来规定浏览器是否可以直接使用缓存中的副本,还是需要去源服务器获取更新的版本。 新鲜度(过期机制):也就是缓存副本有效期。一个缓存副本必须满足以下条件,浏览器会认为它是有效的,足够新的:
- 含有完整的过期时间控制头信息(HTTP协议报头),并且仍在有效期内;
- 浏览器已经使用过这个缓存副本,并且在一个会话中已经检查过新鲜度;
满足以上两个情况的一种,浏览器会直接从缓存中获取副本并渲染。 校验值(验证机制):服务器返回资源的时候有时在控制头信息带上这个资源的实体标签Etag(Entity Tag),它可以用来作为浏览器再次请求过程的校验标识。如过发现校验标识不匹配,说明资源已经被修改或过期,浏览器需求重新获取资源内容。
Pragma
当该字段值为no-cache的时候,会告诉浏览器不要对该资源缓存,即每次都得向服务器发一次请求才行。
res.setHeader('Pragma', 'no-cache') // 禁止缓存
res.setHeader('Cache-Control', 'public,max-age=120') // 2分钟
2
通过Pragma来禁止缓存,通过Cache-Control设置两分钟缓存,但是重新访问我们会发现浏览器会再次发起一次请求,说明了Pragma的优先级高于Cache-Control。
缓存优先级
// Pragma高于强缓存,强缓存高于协商缓存
Pragma > Cache-Control > Expires > ETag > Last-Modified
2
浏览器缓存的控制
使用HTML Meta标签
Web开发者可以在HTML页面的<head>
节点中加入<meta>
标签,代码如下:
<meta HTTP-EQUIV="Pragma" CONTENT="no-cache">
上述代码的作用是告诉浏览器当前页面不被缓存,每次访问都需要去服务器拉取。使用上很简单,但只有部分浏览器可以支持,而且所有缓存代理服务器都不支持,因为代理不解析HTML内容本身。
使用缓存有关的HTTP消息报头
在HTTP请求和响应的消息报头中,常见的与缓存有关的消息报头有:
强缓存(返回的http状态为200)
首先说明一点:Cache-Control与Expires的作用一致,都是指明当前缓存文件的有效期,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。只不过Cache-Control的选择更多,设置更细致,如果同时设置的话,其优先级高于Expires。
Expires
Expires是HTTP1.0时的规范,它的值是服务器返回的一个绝对时间的GMT格式的时间字符串,比如expires: Thu, 05 Jul 2029 11:13:14 GMT
。这个时间代表着这个资源的过期时间,在此时间之前,即命中强缓存。
Expires的缓存原理是:
- 浏览器第一次向服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上Expires的header;
- 浏览器在接收到这个资源后,会把这个资源连同所有response header一起缓存下来(所以缓存命中的请求返回的header并不是来自服务器,而是来自之前缓存的header);
- 浏览器再请求这个资源时,先从缓存中寻找,找到这个资源后,拿出它的Expires跟当前的请求时间比较,如果请求时间在Expires指定的时间之前,就能命中缓存,否则就不行。
- 如果缓存没有命中,浏览器直接从服务器加载资源时,Expires Header在重新加载的时候会被更新。
**Expires有一个明显的缺点:**Expires的值是服务器的时间,假如当前客户端时间和服务器时间相差很大,那误差就很大了。比如服务器返回的是2020年1月13号过期,客户端的时间被修改了,快了一天为2020年1月14号,那客户端缓存就过期了。所以它被Cache-Control:max-age=秒
替代了。
需要注意:如果max-age和Expires同时存在,Cache-Control优先级高于Expires。
Cache-Control头(设置max-age值)
Cache-Control是HTTP1.1时的规范,Cache-Control的max-age规定了缓存的有效时间(距离失效的秒数)。 Cache-Control的缓存原理是:
- 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上Cache-Control的header;
- 浏览器在接收到这个资源后,会把这个资源连同所有response header一起缓存下来;
- 浏览器再请求这个资源时,先从缓存中寻找,找到这个资源后,根据它第一次的请求时间和Cache-Control设定的有效期,计算出一个资源过期时间,再拿这个过期时间跟当前的请求时间比较,如果请求时间在过期时间之前,就能命中缓存,否则就不行。
- 如果缓存没有命中,浏览器直接从服务器加载资源时,Cache-Control Header在重新加载的时候会被更新。
Cache-Control的参数可以设置很多值,可以是: public、private、no-cache、no-store、no-transform、must-revalidate、proxy-revalidate、max-age。 各个消息中的指令含义是:
- public: 表示响应可被任何缓存区缓存
- private: 表示对于单个用户的整个或部分部分响应消息,不能被共享缓存处理。这允许服务器仅仅描述当用户的部分响应消息,此响应消息对于其他用户的请求无效
- no-cache: 告诉浏览器忽略资源的缓存副本,强制每次请求直接发送给服务器,拉取资源,但不是不缓存
- no-store: 用于防止重要的信息被无意的发布,在请求消息中发送将使得请求和响应消息都不使用缓存
- max-age: 表示从当前请求开始,允许获取的响应被重用的最长时间(单位为秒)- 例如:max-age=5000表示响应可以再缓存和重用5000秒。
- min-fresh: 指示客户机可以接收响应时间小于当前时间加上指定时间的响应
- max-stale: 指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可以接收超出超时期间指定之内的响应消息
no-cache和max-age=0区别
- no-cahce并不是表示无缓存,而是指使用缓存一定要先经过验证
- response header的no-cache和max-age=0和request header的max-age=0的作用是一样的:都要求在使用缓存之前进行验证
- request header的no-cache,则表示要重新获取请求,其作用类似于no-store
no-store和no-cache的区别
- no-store:如果服务器再响应中设置了no-store。那么浏览器不会存储这次相应的数据,当下次请求时,浏览器会在请求一次,就是说不会对比Etag;
- no-cache:如果服务器在响应中设置了no-cache,那么说明浏览器在使用缓存前会对比Etag,返回304就会避免修改。
public和private区别
- public:表示该响应可以在用户的浏览器或者任何中继web代理对其进行缓存,不写默认为public;
- private:表示响应不允许任何web代理进行缓存,只有用户的浏览器可以进行缓存。
Cache-Control和Expires区别
- 首先Expires是HTTP1.0的规范,Cache-Control是HTTP1.1的规范。
- Cache-Control描述的是一个相对时间,在进行缓存命中的时候,都是基于当前客户端时间进行判断,所以相比较Expires(是服务器返回的一个绝对时间),Cache-Control的缓存管理更有效,安全一些。
- Expires和Cache-Control同时存在时,Cache-Control优先级高于Expires。 具体参考
协商缓存
Last-Modified与If-Modified-Since
Last-Modified:标识响应资源的最后修改时间,web服务器在响应请求时,告诉浏览器资源的最后修改时间。 Last-Modified/If-Modified-Since的缓存的原理:
- 浏览器第一次向服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上Last-Modified的header,这个header表示这个资源在服务器上的最后修改时间;
- 浏览器再次向服务器请求这个资源时,在request的header上加上If-Modified-Since的header,这个header的值就是上一次请求时返回的Last-Modified的值;
- 服务器再次收到这个资源请求时,根据浏览器传过来If-Modified-Since和资源在服务器上的最后修改时间判断资源是否有变化,如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源内容。当服务器返回304 Not Modified的响应时,response header中不会再添加Last-Modified的header,因为既然资源没有变化,那么Last-Modified也就不会改变;
- 浏览器收到304的响应后,就会从缓存中加载资源;
- 如果协商缓存没有命中,浏览器直接从服务器加载资源时,Last-Modified Header在重新加载的时候会被更新,下次请求时,If-Modified-Since会启用上次返回的Last-Modified值。
需要注意:按下ctrl+r刷新会默认跳过max-age和Expires的检验直接去向服务器发送请求。
核心思想: 当缓存资源过期后(也就是Cache-Control:max-age=0),假设该资源具有Last-Modified声明,则再次向服务器请求时带上头If-Modified-Since,表示请求时间。服务器收到请求后发现有头If-Modified-Since,则与被请求资源的最后修改时间进行比对。若Last-Modified的时间较新,说明最后修改时间较新,说明资源有被改动过,则响应整的资源重新从服务器读取,而不是读取缓存,返回HTTP200;若If-Modified-Since的时间比Last-Modified新或者两者相等,说明服务器的内容没有更新,直接读取缓存即可,返回HTTP304,告知浏览器继续使用所保存的缓存资源,同时更新响应头last-Modified的值(以备下次对比)。
Last-Modified缺点:
- last-modified是以秒为单位的,资源在1s内可能修改多次,那么该缓存就不能被使用的。
- 如果文件是通过服务器动态生成,那么更新的时间永远就是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用。
- 有些文档可能会被周期性地重写,但实际包含的数据常常是一样的。尽管内容没有变化,但是修改日期会发生变化。
- 有些文档可能被修改了,但所做修改并不重要,不需要让世界范围内的缓存都重装数据(比如对拼音或注释的修改)。
- 有些服务器无法准确地判定其页面的最后修改日期。
- 有些服务器提供的文档会在亚秒间隙发生变化,对这些服务器来说,以秒为粒度的修改日期就不够用了(短时间内资源发生了改变,Last-Modified并不会发生变化)。
这个问题就需要下面说到的ETag来解决了。
ETag与If-None-Match
Etag(实体标签): 服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。所以,我们不用管它是怎么生成的,我们只需要清楚:每个文件都有一个唯一的标识,只要这个文件发生了改变,这个标识就会发生变化。 实际上ETag并不是文件的版本号,而是一串可以代表该文件唯一的字符串(Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的)。
ETag/If-None-Match的缓存的原理:
- 浏览器第一次向服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上ETag的header,这个header是服务器根据当前请求的资源生成的一个唯一标识,这个唯一标识是一个字符串,只要资源有变化这个串就不同,跟最后修改时间没有关系,所以能很好解决Last-Modified的问题;
- 浏览器再次向服务器请求这个资源时,在request的header上加上If-None-Match的header,这个header的值就是上一次请求时返回的ETag的值;
- 服务器再次收到资源请求时,根据浏览器传过来If-None-Match和然后再根据资源生成一个新的ETag,如果这两个值相同就说明资源没有变化,否则就是有变化;如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源内容。与Last-Modified不一样的是:当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化;
- 浏览器收到304的响应后,就会从缓存中加载资源,节省了相关资源在网路上的传输时间。
特别注意:ETag与Last-Modified不同,当服务器返回304 Not Modified
的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。
与Last-Modify/If-Modify-Since不同的是:Etag/If-None-Match返回的是一个校验码。ETag可以保证每一个资源是唯一的,资源变化都会导致ETag变化。服务器根据浏览器上送的If-None-Match值来判断是否命中缓存。
Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。
当缓存资源过期之后,如果浏览器发现服务器端返回的头部信息具有Etag声明,就在请求中发送If-None-Match选项,值即为上次请求后响应头的ETag值,该值在服务端和服务端代表该文件唯一的字符串对比(如果服务端该文件改变了,该值就会变),如果相同,则响应HTTP304,客户端直接读取缓存,如果不相同,则响应HTTP200,返回最新的数据资源,更新ETag值。
Last-Modified与ETag两者功能相似甚至相同,为什么要同时存在?
HTTP1.1中ETag的出现主要是为了解决几个Last-Modified比较难解决的问题:
- Last-Modified标注的最后修改只能精确到秒,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间
- 如果某些文件会被定期生成,但有时内容并没有任何变化(仅仅改变了时间),但Last-Modified却改变了,导致文件没法使用缓存
- 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形
哪些请求不能被缓存?
无法被浏览器缓存的请求:
- HTTP信息头中包含Cache-Control:no-cache,pragma:no-cache,或Cache-Control:max-age=0等告诉浏览器不用缓存的请求
- 需要根据Cookie,认证信息等决定输入内容的动态请求是不能被缓存的
- 经过HTTPS安全加密的请求(有人也经过测试发现,ie其实在头部加入Cache-Control:max-age信息,firefox在头部加入Cache-Control:Public之后,能够对HTTPS的资源进行缓存,参考《HTTPS的七个误解》)
- POST请求无法被缓存
- HTTP响应头中不包含Last-Modified/Etag,也不包含Cache-Control/Expires的请求无法被缓存
Last-Modified和ETag与Cache-Control和Expires区别
- Cache-Control/Expires:如果检测到本地的缓存还在有效的时间范围内,浏览器直接使用本地副本,不会发送任何请求。两者一起使用时,Cache-Control/Expires的优先级要高于Last-Modified/ETag。即当本地副本根据Cache-Control/Expires发现还在有效期内时,则不会再次发送请求去服务器询问修改时间(Last-Modified)或实体标识(Etag)了。
- 如果Cache-Control/Expires失效,且配置Last-Modified/ETag的情况下,浏览器再次访问统一URI(统一资源标识符)的资源,还是会发送请求到服务器询问文件是否已经修改,如果没有,服务器会只发送一个304回给浏览器,告诉浏览器直接从自己本地的缓存取数据;如果修改过,那就把最新的文件发给浏览器。
注意: 一般情况下,需要Cache-Control/Expires配合Last-Modified/ETag一起使用,因为即使服务器设置了缓存时间,当用户点击刷新按钮时,浏览器会忽略缓存继续向服务器发送请求,这时Last-Modified/ETag将能够很好利用304,从而减少响应开销(这里只是避免了服务器响应数据,也就是说如果响应304。服务不再返回数据,浏览器直接使用缓存文件,但是浏览器端依旧会发送请求)。
小结
总结 当浏览器再次访问一个已经访问过的资源时,它会这样做:
- 看看是否命中强缓存,如果命中,就直接使用缓存了。
- 如果没有命中强缓存,就发请求到服务器检查是否命中协商缓存。
- 如果命中协商缓存,服务器会返回 304 告诉浏览器使用本地缓存。
- 否则,返回最新的资源。
上图对于浏览器缓存机制描述的很详细,这里我对其做了一下语言描述。 个人理解,如有错误,请指正
首先声明一点:对于第一次请求,无论是静态文件还是其他文件,都需要从服务器返回,并不存在缓存一说。等第一次请求完,浏览器就有缓存了,然后整个的加载过程就完全不一样了。这里重点说明有缓存的情况。 当客户端向服务器发送一个请求时,首先检查是否存在缓存,有缓存文件则判断缓存文件是否过期,如果缓存文件没有过期,则直接从缓存中读取文件。 如果缓存文件过期,则查看响应头中是否存在Etag(优先级高于Last-Modified),如果不存在,则继续查看响应头中是否存在Last-Modified,如果也不存在,则直接向服务器发送新的请求,等待请求响应,缓存协商,呈现页面。 如果缓存文件过期,响应头中存在ETag(实体标签),向服务器发送带If-None-Match的请求,其值即为上次请求后响应头的ETag值,该值在服务端和服务端代表该文件唯一的字符串对比(如果服务器端该文件更新了,该值就会变),如果相同,则响应HTTP304,客户端直接读取缓存文件,如果不相同,则响应HTTP200,服务器返回最新的文件,同时更新ETag值。 如果缓存文件过期,响应头中不存在Etag,但是存在Last-Modified,则向服务器发送带If-Modified-Since的请求,将If-Modified-Since的日期和服务端该文件的最后修改日期对比,如果两个日期相同,则响应HTTP304,客户端直接读取缓存文件;如果不相同则表示文件发生变化更新,响应HTTP200,从服务器返回最新的文件数据,同时更新响应头last-Modified的值(以备下次对比)。