浏览器缓存机制

bug描述: tab页票据类型定义刷新无效

定位分析

其他tab页签没有这个问题, 为jsp老页面, 出现问题为vue打包新页面

点击刷新按钮调用的jquery下的attr方式, 即重新修改src链接地址

刷新方法执行后无请求访问, 没有重新请求资源

原因分析

从没有重新请求资源这点来看, 是否是因为缓存问题?

方法调用是否有问题?

vue页面和jsp页面是否有差别导致行为不同?

诊断过程

尝试1 - 添加时间戳

分析: 认为因为直接修改的src地址没有发生改变, 导致走缓存故没有发生请求

结果: 添加上时间戳问题依然存在, 没有起什么作用

判断: 刷新方式修改src调用没有出现错误 --> 排除原因2 && 1

尝试2 - 本地引入vue打包页面和线上jsp, 替换src模拟问题

分析: 是否因为jsp和单页面的html有什么区别导致的

结果: 本地同样调用setAttribute替换src(无时间戳), 正常刷新加载

判断: vue页面和jsp行为一致, 并没有不同 --> 排除原因3

问题排除完了, 没解决, 何解? ==> 尴尬 好像还有一个没有尝试

尝试3 - 本地引入问题页面(iframe线上地址)刷新尝试, 问题重现

同样都是vue页面, 没区别, 方法没区别, 环境没区别, 结果却不一样?emmmmm :laughing:

好像区别来了。 Etag Last-Modified 难道不是因为url没有变化导致的缓存问题, 而是服务端配置的缓存问题?

缓存类型

很多时候,大家倾向于将浏览器缓存简单地理解为“HTTP 缓存”。但事实上,浏览器缓存机制有几个方面,它们按照获取资源时请求的优先级依次排列如下:

Memory Cache

Disk Cache

Service Worker Cache

HTTP Cache (Cache-Control、expires)

强缓存

协商缓存

Push Cache

HTTP Cache (请求缓存)

强缓存

强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的 expires 和 cache-control 判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信。

命中强缓存的情况下,返回的 HTTP 状态码为 200 (如下图)。

Expires

// expires: Wed, 11 Sep 2019 16:12:18 GMT

HTTP 1.0,设置缓存的截止时间,在此之前,浏览器对缓存的数据不重新发请求。它与Last-Modified/Etag结合使用,用来控制请求文件的有效时间,当请求数据在有效期内,浏览器从缓存获得数据。Last-Modifed/Etag能够节省一点宽带,但是还会发一个HTTP请求

如果服务端和客户端的时间设置可能不同,或者我直接手动去把客户端的时间改掉,那么 expires 将无法达到我们的预期。

ps: 可能偶尔还会遇到pragma这个字段

Pragma

是一个在 HTTP/1.0 中规定的通用首部,这个首部的效果依赖于不同的实现,所以在“请求-响应”链中可能会有不同的效果。它用来向后兼容只支持 HTTP/1.0 协议的缓存服务器,那时候 HTTP/1.1 协议中的 Cache-Control 还没有出来。

由于 Pragma 在 HTTP 响应中的行为没有确切规范,所以不能可靠替代 HTTP/1.1 中通用首部 Cache-Control,尽管在请求中,假如 Cache-Control 不存在的话,它的行为与 Cache-Control: no-cache 一致。建议只在需要兼容 HTTP/1.0 客户端的场合下应用 Pragma 首部。

Cache-Control

// cache-control: max-age=3600, s-maxage=31536000

HTTP 1.1,设置资源在本地缓存多长时间。

Cache-Control 相对于 expires 更加准确,它的优先级也更高。当 Cache-Control 与 expires 同时出现时,我们以 Cache-Control 为准。

Cache-Control字段中可以声明多些元素,例如no-cache, must-revalidate, max-age=0等。这些元素用来指明页面被缓存最大时限,如何被缓存的,如何被转换到另一个不同的媒介,以及如何被存放在持久媒介中的。但是任何一个 Cache-Control指令都不能保证隐私性或者数据的安全性。“private”和“no-store”指令可以为隐私性和安全性方面提供一些帮 助,但是他们并不能用于替代身份验证和加密。

s-maxage/maxage: s-maxage优先级高于max-age,两者同时出现时,优先考虑 s-maxage。如果 s-maxage 未过期,则向代理服务器请求其缓存内容, s-maxage只针对public共享缓存,仅在代理服务器生效。

public/private: 是针对资源是否能够被代理服务缓存而存在的一组对立概念, public能被代理服务器缓存, private只能被浏览器缓存, 即私有缓存和共享缓存。

no-stor/no-cache: no-cache不允许任何一级缓存直接使用未经目标服务器验证的缓存响应来响应请求, no-store不允许任何一级对该响应进行保存

must-revalidate: 如果你配置了max-age信息,当缓存资源仍然新鲜(小于max-age)时使用缓存,否则需要对资源进行验证。所以must-revalidate可以和max-age组合使用Cache-Control: must-revalidate, max-age=60

协商缓存

协商缓存即浏览器与服务器合作之下的缓存策略, 浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304(如下图)。

Last-Modified和ETag是条件请求(Conditional Request)相关的两个字段。如果一个缓存收到了针对一个页面的请求,它发送一个验证请求询问服务器页面是否已经更改,在HTTP头里面带上ETag和If Modify Since头。服务器根据这些信息判断是否有更新信息,如果没有,就返回HTTP 304(Not Modify);如果有更新,返回HTTP 200和更新的页面内容,并且携带新的ETag和Last-Modified。

使用这个机制,能够避免重复发送文件给浏览器,不过仍然会产生一个HTTP请求

Last-Modified / If-Modified-Since / If-Unmodified-Since

// Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT// If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT

当浏览器第一次请求一个url时,服务器端的返回状态码为200,同时HTTP响应头会有一个Last-Modified标记着文件在服务器端最后被修改的时间。

浏览器第二次请求上次请求过的url时,浏览器会在HTTP请求头添加一个If-Modified-Since的标记,用来询问服务器该时间之后文件是否被修改过。

如果服务器端的资源没有变化,则自动返回304状态,使用浏览器缓存,从而保证了浏览器不会重复从服务器端获取资源,也保证了服务器有变化是,客户端能够及时得到最新的资源。

If-Modified-Since 是一个条件式请求首部,服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的 304 响应,而在 Last-Modified 首部中会带有上次修改时间。 不同于 If-Unmodified-Since, If-Modified-Since 只可以用在 GET 或 HEAD 请求中。

当与 If-None-Match 一同出现时,它(If-Modified-Since)会被忽略掉,除非服务器不支持 If-None-Match

Etag / If-None-Match

ETagHTTP响应头是资源的特定版本的标识符。这可以让缓存更高效,并节省带宽,因为如果内容没有改变,Web服务器不需要发送完整的响应。而如果内容发生了变化,使用ETag有助于防止资源的同时更新相互覆盖(“空中碰撞”)。Etag 在感知文件变化上比 Last-Modified 更加准确,优先级也更高。当 Etag 和 Last-Modified 同时存在时,以 Etag 为准。

当浏览器第一次请求一个url时,服务器端的返回状态码为200,同时HTTP响应头会有一个Etag,存放着服务器端生成的一个序列值。

浏览器第二次请求上次请求过的url时,浏览器会在HTTP请求头添加一个If-None-Match的标记,用来询问服务器该文件有没有被修改。

前面可以加上 W/ 前缀表示应该采用弱比较算法。

Etag 主要为了解决 Last-Modified 无法解决的一些问题:

一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;

某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒)

某些服务器不能精确的得到文件的最后修改时间

对比

用户操作与缓存(√: 有效 ×: 无效)

如果失效日期Cache-Control : max-ag=0或者是负值,浏览器会在对应的缓存中把Expires设置为1970-01-01 08:00:00。

如果max-age和expires属性都没有,找找头里的Last-Modified信息。如果有,缓存的寿命就等于头里面Date的值减去Last-Modified的值除以10(注:根据rfc2626其实也就是乘以10%)

httph缓存策略

第一次请求

第二次请求

其他

Range

The Range 是一个请求首部,告知服务器返回文件的哪一部分。在一个 Range 首部中,可以一次性请求多个部分,服务器会以 multipart 文件的形式将其返回。如果服务器返回的是范围响应,需要使用 206 Partial Content 状态码。假如所请求的范围不合法,那么服务器会返回 416 Range Not Satisfiable 状态码,表示客户端错误。服务器允许忽略 Range 首部,从而返回整个文件,状态码用 200 。

If-Range HTTP 请求头字段用来使得 Range 头字段在一定条件下起作用:当字段值中的条件得到满足时,Range 头字段才会起作用,同时服务器回复206 部分内容状态码,以及Range 头字段请求的相应部分;如果字段值中的条件没有得到满足,服务器将会返回 200 OK 状态码,并返回完整的请求资源。

Age

Age 消息头里包含消息对象在缓存代理中存贮的时长,以秒为单位。.

Age消息头的值通常接近于0。表示此消息对象刚刚从原始服务器获取不久;其他的值则是表示代理服务器当前的系统时间与此应答消息中的通用消息头 Date 的值之差。

Vary

Vary HTTP 响应头决定了对于后续的请求头,如何判断是请求一个新的资源还是使用缓存的文件。

当缓存服务器收到一个请求,只有当前的请求和原始(缓存)的请求头跟缓存的响应头里的Vary都匹配,才能使用缓存的响应。

MemoryCache (内存缓存)

MemoryCache顾名思义,就是将资源缓存到内存中,等待下次访问时不需要重新下载资源,而直接从内存中获取。Webkit早已支持memoryCache。 目前Webkit资源分成两类,一类是主资源,比如HTML页面,或者下载项,一类是派生资源,比如HTML页面中内嵌的图片或者脚本链接,分别对应代码中两个类:MainResourceLoader和SubresourceLoader。虽然Webkit支持memoryCache,但是也只是针对派生资源,它对应的类为CachedResource,用于保存原始数据(比如CSS,JS等),以及解码过的图片数据。

DiskCache (磁盘缓存)

diskCache顾名思义,就是将资源缓存到磁盘中,等待下次访问时不需要重新下载资源,而直接从磁盘中获取,它的直接操作对象为CurlCacheManager。它与memoryCache最大的区别在于,当退出进程时,内存中的数据会被清空,而磁盘的数据不会,所以,当下次再进入该进程时,该进程仍可以从diskCache中获得数据,而memoryCache则不行。 diskCache与memoryCache相似之处就是也只能存储一些派生类资源文件。它的存储形式为一个index.dat文件,记录存储数据的url,然后再分别存储该url的response信息和content内容。Response信息最大作用就是用于判断服务器上该url的content内容是否被修改

资源本身大小数值:当http状态为200是实实在在从浏览器获取的资源,当http状态为304时该数字是与服务端通信报文的大小,并不是该资源本身的大小,该资源是从本地获取的

chrome采取措施的准则:

一般图片会用disk cache, js文件用memory cache, 具体使用哪种根据浏览器的节约原则来决定(文件大小和加载情况)

先去内存看,如果有,直接加载

如果内存没有,择取硬盘获取,如果有直接加载

如果硬盘也没有,那么就进行网络请求

加载到的资源缓存到硬盘和内存

Service Worker Cache (离线缓存)

Service Worker 是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM。这样独立的个性使得 Service Worker 的“个人行为”无法干扰页面的性能,这个“幕后工作者”可以帮我们实现离线缓存、消息推送和网络代理等功能。我们借助 Service worker 实现的离线缓存就称为 Service Worker Cache。

PS:大家注意 Server Worker 对协议是有要求的,必须以 https 协议为前提。

Push Cache (推送缓存)

“推送缓存”是针对HTTP/2标准下的推送资源设定的。推送缓存是session级别的,如果用户的session结束则资源被释放;即使URL相同但处于不同的session中也不会发生匹配。推送缓存的存储时间较短,在Chromium浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令

几乎所有的资源都能被推送,并且能够被缓存。测试过程是在推送资源之后尝试用fetch()、XMLHttpRequest、<\link rel="stylesheet" href="…">、<\script src="…">、<\iframe src="…">获取推送的资源。Edge和Safari浏览器支持相对比较差

no-cache和no-store资源也能被推送

如果连接被关闭则Push Cache被释放

Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的情况下才会去询问 Push Cache。

多个页面可以使用同一个HTTP/2的连接,也就可以使用同一个Push Cache。这主要还是依赖浏览器的实现而定,出于对性能的考虑有的浏览器会对相同域名但不同的tab标签使用同一个HTTP连接。

Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。

一旦Push Cache中的资源被使用即被移除

你可以为其他域名推送资源

更多

问题回归

认识以上这些缓存内容和顺序后, 好像可以重新来定义下问题了?

问题页面的流程:

Cache-Control: max-age=0 => 缓存过期

Last-Modified/Etag => 协商缓存 => 304, 不重新请求资源

说白了, 不管有没有详细了解原因, 解决事情的方法也许并不只有一种, 既然index.html和其他jsp采用了不同的缓存机制, 单独再去调整服务器的静态资源配置好像比较麻烦。

这个页面的刷新刷新, 其实主要就是为了简化页面的行为, 重新加载文档, 而这里因为缓存原因导致的异常行为主要是因为替换src的这个方法。那么如果我换个方法, 改用reload(), 是否就足够达到目的。

尝试了下, 好像没什么问题, 解决√

那么... 其实就是换个方法你扯这么多缓存的问题干嘛?

其实缓存这部分都是后面补充的, 分析到问题发现Etag和Last-Modified的时候定位到原因应该如此

对于缓存大概有些零散的概念和字段的了解,却都没有足够系统详细化

后续杂谈

等等, 原本我以为, 一切都这么解决了。却还有一些避开的疑问

如果是因为etag和last modify导致走缓存, 那么同样至少也应该有一个请求验证etag的请求

而实际我们看到的却是,不仅没有任何请求,页面也没有作重新加载

问题是解决了,既然想要作分享,说得稀里糊涂不如打破砂锅问到底

为此我又做了一些其他尝试

nginx代理本地页面模拟, 开启etag和last modify, 页面同样跳转(替换src)

我开始有些陷入僵局, 如此一来,好像逐渐变得和缓存没有关系

因为即便不设置etag和lastmodify也好,同样from cache, 也加载了, 这样一来缓存一说更站不住脚

于是我开始折腾起了src被替换的问题。

iframe替换src这个行为到底是怎么样的?

这时候我想到了另外一个东西, window.location.href

是否这两者类似, 或者说,其实可能设置src, 就是在调用location.href方法

为此我做了下尝试: window.location.href = window.location.href

结果: 问题页面依旧不刷新, 而普通页面正常刷新

一时语塞,稍加思索,冷静思考,欲言又止.jpg

等等,好像有什么不一样?

我在普通页面执行直接刷新, 在问题页面执行不刷新, 但却会打印出url

我们知道, 第二句话是函数执行的返回值(赋值操作的赋值)

那么, 是否是因为调用window.location.href的行为被修改所导致的

基于此我又在项目内搜索了locaiton.href.除了打包的vender.js内并未找到对应关键字

项目内部没有修改, 说到底我不可能直接把window.location.href指向一个函数

如果真要修改这个行为, 也只能...Object.defineProperty设置set方法拦截默认行为

我又想到了另外一个尝试,打开了与项目无关,另一个vue打包的页面, 调用该方法

原因get√? 你找到原因了 ?

这个原因深究下去就可能又是另一个主题了。

明白的人大概已经在最后尝试验证的对比下明白了。

不明白的, 为什么只有2个vue页面会如此 ?

最后提醒

真的需要刷新页面这种操作的时候, vue页面最好采用reload方法直接重载, 而不是修改src, 即便带有时间戳的查询字符串想要去触发刷新行为也会因为vue(router)的数据拦截url没有发生变化导致不触发

Cache - Control 缓存指令

缓存请求指令:

Cache-Control: max-age=

Cache-Control: max-stale[=]

Cache-Control: min-fresh=

Cache-control: no-cache

Cache-control: no-store

Cache-control: no-transform

Cache-control: only-if-cached

缓存响应指令

Cache-control: must-revalidate

Cache-control: no-cache

Cache-control: no-store

Cache-control: no-transform

Cache-control: public

Cache-control: private

Cache-control: proxy-revalidate

Cache-Control: max-age=

Cache-control: s-maxage=

扩展Cache-Control指令

Cache-control: immutable

Cache-control: stale-while-revalidate=

Cache-control: stale-if-error=

指令

可缓存性

public: 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存。

private: 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它),可以缓存响应内容。

no-cache: 在释放缓存副本之前,强制高速缓存将请求提交给原始服务器进行验证。

only-if-cached: 表明客户端只接受已缓存的响应,并且不要向原始服务器检查是否有更新的拷贝

到期

max-age=: 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间。

s-maxage=: 覆盖max-age 或者 Expires 头,但是仅适用于共享缓存(比如各个代理),并且私有缓存中它被忽略。

max-stale[=]: 表明客户端愿意接收一个已经过期的资源。 可选的设置一个时间(单位秒),表示响应不能超过的过时时间。

min-fresh=: 表示客户端希望在指定的时间内获取最新的响应。

stale-while-revalidate=: 表明客户端愿意接受陈旧的响应,同时在后台异步检查新的响应。秒值指示客户愿意接受陈旧响应的时间长度。

stale-if-error=: 表示如果新的检查失败,则客户愿意接受陈旧的响应。秒数值表示客户在初始到期后愿意接受陈旧响应的时间。

重新验证和重新加载

must-revalidate: 缓存必须在使用之前验证旧资源的状态,并且不可使用过期资源。

proxy-revalidate: 与must-revalidate作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略。

immutable: 表示响应正文不会随时间而改变。资源(如果未过期)在服务器上不发生改变,因此客户端不应发送重新验证请求头(例如If-None-Match或If-Modified-Since)来检查更新,即使用户显式地刷新页面。在Firefox中,immutable只能被用在 https:// transactions.

其他

no-store: 缓存不应存储有关客户端请求或服务器响应的任何内容。

no-transform: 不得对资源进行转换或转变。Content-Encoding, Content-Range, Content-Type等HTTP头不能由代理修改。例如,非透明代理可以对图像格式进行转换,以便节省缓存空间或者减少缓慢链路上的流量。 no-transform指令不允许这样做。

参考链接:

设计一个无懈可击的浏览器缓存方案

浏览器缓存机制介绍与缓存策略剖析-小册

MDN Cache-Control

memory cache 和 disk cache

HTTP Header

浏览器缓存详解

来源公司技术分享--陈永钦

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335