阅读 7837

浅聊HTTP缓存 (HTTP Cache)

1.引子

HTTP缓存一直是一个老生常谈的问题,前端在日常发布、部署工作中,常常要面对。

其中面对的问题有可能会是:部署的代码无法生效

这次本人所在团队也遇到了相关问题,这里简述一下:

  • 项目会在静态资源(如:css,js)使用chunkHash来处理,因此能保证修改后与旧代码文件名字不会重复。以避免无法更新改动。

  • 在该项目中部署后,进行代码进行一次location.reload,改动即可以生效。

最后,本人发现是因为该项目部署的服务器上所有静态资源的response headers的设置如下:

response headers

  • cache-control: public, max-age=31536000

但致命的是,项目的入口: index.html也是如此。因此实际是因为所有的.html文件命中(cache hit)了强缓存,导致了用户无法直接呈现更新后代码的改动。

找到了原因,也想到了如下三个解决方案

  • 跳转时增加时间戳例如:

    具体为什么可以这么做在之后分析查看是否存在缓存步骤时会解析

    location.href = 'https://www.localhost:5000.com/index.html?t=201811141248001';
    复制代码
  • 修改response headers中的cache-control

    举例:

    cache-control: public, max-age=0
    复制代码
  • 使用HTML Meta 标签

    可以在html代码中增加meta标签:

    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="Expires" content="0" />
    复制代码

    上述代码的作用是告诉浏览器当前页面不被缓存,每次访问都需要去服务器拉取。使用上很简单,但只有部分浏览器可以支持,而且所有缓存代理服务器都不支持,因为代理不解析HTML内容本身。

    最好还是不要指定HTML标签,通过可能会出现混乱(到底以那端为主,实际response header的优先级更高)。此外,在HTML5中,这些<meta HTTP等>标签是无效的。只有HTML5规范中列出的HTTP等效值才被允许。

    可参考:W3 HTML spec chapter 5.2.2

2. HTTP缓存基本概念

既然找到问题了,我觉得那我就顺藤摸瓜的总结一下吧。

  • HTTP 缓存:重用已获取的资源能够有效的提升网站与应用的性能。Web 缓存能够减少延迟与网络阻塞,进而减少显示某个资源所用的时间。借助 HTTP 缓存,Web 站点变得更具有响应性。

  • HTTP 缓存分为:强缓存和协商缓存

(1) 简化流程

其中关键步骤是:

  • 判断是否存在缓存
  • 判断缓存是否有效(即强缓存是否命中)
  • 请求服务端,判断服务端资源是否更新(即协商缓存是否命中
  • 返回资源(若服务端返回的资源,本地保存请求,包括请求头信息)

(2) 查看是否存在缓存

浏览器怎么判定是否存在本地缓存,这个步骤在此可以理解为浏览器去查找本地是否存在该响应请求的文件存在,查找是否是否有该对应的请求,不同浏览器缓存文件的地址也不尽相同。

以firefox举例,可以在地址栏输入:about:cache

P.S: chrome://cache 在 chrome66版本后已废弃。

如图上所示,这里我们可以看到浏览器关于网络请求缓存的一些信息。我们以本地磁盘中的为例子。

如图上所示,在此我们可以查看到一些关于缓存在磁盘内的信息,包括实际本地缓存所在的位置等。

如上图,这就是一次响应请求的文件,并且他会记录完整的url包括:query string。

如上两图所见,我们改变了t参数的值,实际我们在url后打时间戳来规避命中缓存,实际就是在此改变了查询URL,让浏览器无法查询到与之前请求相同的本地缓存。

最后可以看到,我们的本地缓存文件内,会包含response-head的信息,之后的缓存策略和流程都需要依赖此处的信息。

总结一下,查看是否存在缓存的过程实际就是查找响应请求文件是否存在

(3) 强缓存

[1] 强缓存概念

强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程。

实际就是我们整体流程内的,查看是否存在缓存以及,查看缓存是否有效。

[2] 如何实现强缓存

  • 实现强缓存,主要是根据客户端保留的一个服务器端的response header中的两个字段:expirescache-control

  • cache-control优先级比expires

如图:

图中可知两者的区别

  • HTTP响应报文中expires的时间值,是一个绝对值。

  • HTTP响应报文中Cache-Control为max-age=31536000,是相对值。

在无法确定客户端的时间是否与服务端的时间同步的情况下,Cache-Control相比于expires是更好的选择,所以同时存在时,只有Cache-Control生效。

Expires

  • Expires是HTTP/1.0控制网页缓存的字段,其值为服务器返回该请求结果缓存的到期时间,即再次发起该请求时,如果客户端的时间小于Expires的值时,直接使用缓存结果。

    Expires: Wed, 21 Oct 2015 07:28:00 GMT
    复制代码

Cache-Control

在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存,列几个常见的值:

  • public:所有内容都将被缓存(客户端和代理服务器都可缓存)

  • private:所有内容只有客户端可以缓存,Cache-Control的默认取值

  • no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定

  • no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存

  • max-age=xxx (xxx is numeric):缓存内容将在xxx秒后失效

    Cache-Control:public, max-age=31536000
    复制代码

判断缓存是否过期的流程的流程:

缓存失效时间计算公式如下:

expirationTime = responseTime + freshnessLifetime - currentAge
复制代码

在上面这个公式里,responseTime 表示浏览器接收到此响应的那个时间点。

[3] 如何判断强缓存是否命中

状态码为灰色的请求则代表使用了强制缓存,请求对应的Size值则代表该缓存存放的位置

至于from memory cache 和 from disk cache相关的之后讲解。

(4) 协商缓存

[1] 协商缓存概念

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程。

[2] 如何实现协商缓存

  • 协商缓存的标识也是在响应报文的HTTP头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:Last-Modified / If-Modified-SinceEtag / If-None-Match

  • Etag / If-None-Match 优先级比 Last-Modified / If-Modified-Since 高。

Last-modified:

Last-Modified是服务器响应请求时,返回该资源文件在服务器最后被修改的时间

Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
复制代码

If-Modified-Since:

If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT 
复制代码

Etag:

Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成)

ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
ETag: W/"0815"
复制代码

If-None-Match:

If-None-Match是客户端再次发起该请求时,携带上次请求返回的唯一标识Etag值,通过此字段值告诉服务器该资源上次请求返回的唯一标识值。服务器收到该请求后,发现该请求头中含有If-None-Match,则会根据If-None-Match的字段值与该资源在服务器的Etag值做对比,一致则返回304,代表资源无更新,继续使用缓存文件;不一致则重新返回资源文件,状态码为200

If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
复制代码

然后我们看一下具体流程:

[4] 如何判断协商缓存是否命中

如果协商缓存命中,请求响应返回的http状态为304并且会显示一个Not Modified的字符串。

(5) 两者异同

  • 两者的共同点是:如果命中,都是从客户端缓存中加载资源,而不是从服务器加载资源数据;

  • 两者的区别是:强缓存不发请求到服务器,协商缓存会发请求到服务器。

4.相关浏览操操作

其实浏览器的相关操作,会对开发人员理解浏览器HTTP缓存产生一些影响,因此我们详细来分析一下:

在Alloy Team的Web缓存机制系列中有总结:

Web缓存机制系列2 – Web浏览器的缓存机制 - Alloy Team

浏览器相关操作 Expires/Cache-Control Last-Modified / Etag
地址栏回车 有效 有效
页面链接跳转 有效 有效
新开窗口 有效 有效
前进、后退 有效 有效
刷新 无效 有效
强制刷新 无效 无效

这块我想梳理一下,与大家分享以及验证一下:

测试前提:

  • 服务端设置相应的response header,
  • 相应资源都已经加载完毕第一次(如果测试结果相同,测试结果的图片就复用了):

测试的浏览器为:

  • Chrome 70
  • Firefox 63.0.1
  • Opera 56.0

测试影响的文件:

  • index.html (主页面)
  • index.js (js资源)
  • index.css (样式文件)
  • doge.jpeg (图片文件)
  • favicon.ico (图标文件)
  • temp.html(跳转辅助页面,不设置response header且,不在统计范围内)

测试使用的response header的设置为:

  • Cache-Control: max-age=300 // 缓存5分钟
  • ETag: 33a64df551425fcc55e4d42a148795d9f25f89d4 // 服务端固定返回
  • Expires: Fri Nov 16 2018 09:33:01 GMT+0800 (CST) // 缓存5分钟
  • Last-Modified: Wed, 21 Oct 2018 07:28:00 GMT // 服务端固定返回

(1) 页面链接跳转

Chrome 70测试结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js: 命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:未显示(使用抓包工具抓包,未发出请求)

Firefox 63.0.1 测试结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:命中强缓存(使用抓包工具抓包,未发出请求)

Opera 56.0 测试结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:未发出请求(使用抓包工具抓包,无请求)

(2) 新开窗口

Chrome 70测试(由于默认为google页,采用了隐私模式测试)结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:未发出请求(使用抓包工具抓包,无请求)

Firefox 63.0.1 测试结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:命中强缓存(使用抓包工具抓包,未发出请求)

Opera 56.0(隐私模式) 测试结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:未发出请求(使用抓包工具抓包,无请求)

(4) 前进、后退

Chrome 70测试结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:未发出请求(使用抓包工具抓包,无请求)

Firefox 63.0.1 测试结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:命中强缓存(使用抓包工具抓包,未发出请求)

Opera 56.0 测试结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:未发出请求(使用抓包工具抓包,无请求)

(5) 刷新

刷新这一块是测试的重点(之前正因为)

Chrome 70测试结果:

如图:

结果:

  • index.html:命中协商缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:没有命中缓存,服务端获取资源

Firefox 63.0.1 测试结果:

如图:

结果:

  • index.html:命中协商缓存
  • index.js:命中协商缓存
  • index.css:命中协商缓存
  • doge.jpeg:命中协商缓存
  • favicon.ico:命中协商缓存

Opera 56.0 测试结果:

如图:

结果:

  • index.html:命中协商缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:未发出请求(使用抓包工具抓包,无请求)

(6) 地址栏回车

这个部分还得分成从当前tab回车和从另一url

  • Chrome 70测试结果:

[1] 从另一url跳转

相当于页面链接跳转

[2] 当前url回车

相当于刷新

Firefox 63.0.1 测试结果:

[1] 从另一url跳转

相当于页面链接跳转

[2] 当前url回车

相当于页面链接跳转

Opera 56.0 测试结果:

[1] 从另一url跳转

[2] 当前url回车

相当于刷新

(6) 强制刷新

Chrome 70测试结果:

如图:

结果:

  • index.html:从服务器端获取资源
  • index.js:从服务器端获取资源
  • index.css:从服务器端获取资源
  • doge.jpeg:从服务器端获取资源
  • favicon.ico:从服务器端获取资源

Firefox 63.0.1 测试结果:

如图:

结果:

  • index.html:从服务器端获取资源

  • index.js:从服务器端获取资源

  • index.css:从服务器端获取资源

  • doge.jpeg:从服务器端获取资源

  • favicon.ico:从服务器端获取资源

  • Opera 56.0 测试结果:

如图:

结果:

  • index.html:从服务器端获取资源
  • index.js:从服务器端获取资源
  • index.css:从服务器端获取资源
  • doge.jpeg:从服务器端获取资源
  • favicon.ico:从服务器端获取资源

(7)最终总结:

虽然费了那么大力气测试,最终结论只是稍微调整了一下:

浏览器相关操作 Expires/Cache-Control Last-Modified / Etag
页面链接跳转 有效 有效
新开窗口 有效 有效
前进、后退 有效 有效
刷新 chrome opera html无效 ico文件无效,
ff有效
chrome opera ico文件无效,
ff有效
地址栏回车 当前URL回车 - chrome opera同刷新
当前URL回车 - ff同刷新
其他URL回车 - 同页面链接跳转
当前URL回车 - chrome opera同刷新
当前URL回车 - ff同刷新
其他URL回车 - 同页面链接跳转
强制刷新 无效 无效

6. 相关HTTP相关的头字段总结

图片引用自:Web缓存机制系列2 – Web浏览器的缓存机制

7.完整流程图

总结了一个相对完成的流程图:

8. 本文一些不足之处

(1) 分布式系统相关

这部分没有实际实践过,只是摘选了部分文章的观点:

  • 分布式系统里多台服务器间的文件的Last-Modified必须保持一致,以免负载均衡到不同服务器导致对比结果不一致。

  • 分布式系统尽量关闭掉ETag(每台机器生成的ETag都会不一样,淘宝页面中的静态资源response headers中都没有ETag)。

(2) 缓存的不同来源相关

这个部分目前暂时没有找到十分的标准答案或文档,目前我仅将自己梳理过的部分知识记录在案:

其实webkit缓存机制还有一个叫 pageCache 这里暂不讨论:WebKit Page Cache I – The Basics

Chrome使用两个缓存: disk cachememory cache

以下例子都仅针对Chrome

[1] disk cache

从磁盘中获取缓存资源,等待下次访问时不需要重新下载资源,而直接从磁盘中获取。

[2] memory cache

从内存中获取资源,等待下次访问时不需要重新下载资源,而直接从内存中获取。Webkit早已支持memoryCache。

[3]浏览器如何区分使用两者呢?

测试条件与上文其他测试相同:

a. 当前tabs生命周期未结束

b. 当前tabs生命周期结束

可以得出一个基本**“现象”**:

memory cache的生命周期于tabs的选项卡大致对应。

The lifetime of an in-memory cache is attached to the lifetime of a render process, which roughly corresponds to a tab.

可以参考:developer.chrome - webRequest

c. 有疑问之处

有见过一种论点:

目前Webkit资源分成两类:

  1. 主资源

    主资源: 通过MainResourceLoader加载,如HTML页面,或者下载项等

  2. 派生资源

    派生资源:,通过 SubresourceLoader加载,比如HTML页面中内嵌的图片或者脚本链接

虽然Webkit支持memoryCache,但是也只是针对派生资源,它对应的类为CachedResource,用于保存原始数据(比如CSS,JS等),以及解码过的图片数据。

此图所示:

好像并不适用,完全适用css第一次并没有,从 memory cache 加载,

但是经过几次,后退重新跳转后(不定次数):

到此,以本人的能力可能暂时,无法作出一个比较好的解答了,希望之后有大佬可以给到一个解答。

9. 供实践的Demo

Demo仓库地址

参考文献: