深入理解HTTP Cache(HTTP Caching译文)
公司项目为使用Angular,React,非单页面应用。每个页面要发多个请求,页面加载缓慢。为此,学习下HTTP Cache。
通过网络请求获取资源既慢又昂贵。大量的请求在服务端和客户端之间往返,使得资源可用时间以及浏览器可处理它们的时间都有了延迟,同时用户访问的数据成本也会增加。因此,缓存和重用已获取的资源是优化前端性能的一个关键点。
当前,所有的浏览器都附带实现了HTTP缓存。所以我们只需要在服务端返回正确的HTTP header指令来告诉浏览器当前response的缓存时间就可以了。
Tips:
如果你在你的应用中是使用Webview来获取和显示内容,你可能需要添加其他配置以确保启用浏览器缓存, 它的大小应该与你的用例匹配,缓存持久有效。查阅相关平台文档确保配置准确无误。
当服务端返回响应时,同时附带发出一个HTTP header的集合,描述了它的content-type(内容类型)、Content-length(长度)、缓存指令以及验证令牌等等。如上图,服务端返回了一个长度为1024字节的response,并且提供了一个验证令牌(“x234dff”
),ETag
用于在响应过期后验证资源是否已被修改。
一、使用ETags
验证缓存的response
- 服务器使用http header
ETag
来传递验证令牌; - 有了验证令牌,可以有效的进行资源更新的检查; 如果资源没有更改,则不会传输任何数据。
例如:
浏览器发起上一次的get请求已经超过了120s(Cache-Control:max-age=120)
,并且浏览器发起对同一资源的新请求。
首先,浏览器检查本地缓存并找到先前的响应。但是响应已过期,无法使用之前的response。
此时,浏览器可以发起新的请求并获取新的完整的响应结果,但是效率相对低下,因为资源并没有被修改,没有必要重复下载已经存在缓存中的资源。
这就是ETag
会解决的问题。服务器生成并返回任意ETag
,该ETag
通常是文件内容的散列或其他指纹。客户端不需要知道ETag
是如何生成的;它只需要在下一个请求时将其发送到服务器。如果ETag
的值仍然相同,则资源未更改,可以跳过下载,访问本地缓存资源。
如上图,客户端在HTTP请求头中自动提供了ETag'
在If-None-Match
中。服务端根据当前所访问的资源检查ETag
令牌。如果令牌没有改变,则服务端返回304 Not Modified
,告诉浏览器当前所访问的资源并没有发生修改,可以继续用浏览器中的缓存,并将缓存时间重新计时120s。此时,浏览器不会在下载response,可以节省时间和带宽。
作为Web开发人员,如何进行有效的重新验证呢?在这里,浏览器已经帮我们完成了全部工作。浏览器会自行检测先前是否已经指定验证令牌,并在发出请求时将验证令牌附加到Http请求头中,并根据从服务端收到的response更新缓存时间戳。唯一需要做的事情是需要确保服务端会提供必要的ETag
令牌。
Tips:
Github项目 [h5bp/server-configs](https://github.com/h5bp/server-configs)包含了流行服务器的配置文件示例代码,并配有详细的注释。你可以在其中找到自己所使用的服务器,并查找相应的配置来确认自己的服务器配置是否准确。
二、Cache-Control
- 每个资源都可以通过http header
Cache-Control
来定义其缓存策略。 -
Cache-Control
指令指明了谁来缓存响应、缓存条件以及缓存时间。
从性能优化的角度来看,最好的请求是不需要与服务端进行交互——一个本地的响应副本可以消除所有的网络延时并避免数据传输时的数据费用。 因此,HTTP规范允许服务器返回[Cache-Control]
(https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9)指令,该指令用来控制浏览器及其他中间缓存件如何缓存单个响应以及缓存多长时间。
Tips:
Cache-Control标头被定义为HTTP / 1.1规范的一部分,并且取代了原先用于定义response缓存策略的
Expire等,现在所有的现代浏览器都支持HTTP Cache-Control。
2.1 “no-cache”和“no-store”
no-cache:有缓存,但是不直接使用缓存,需要经过校验。
如果资源已经发生更改,在没有与服务端进行校验前,浏览器不能用前面返回的response用于满足对同一URL的后续请求。如果浏览器提供了正确的ETag
,当配置了no-cache
时,客户端会先发出请求向后端验证资源是否发生更改,如果资源未更改,则可以取消下载。
no-store:完全没有缓存,所有的资源都需要重新发请求
当服务端对资源设置了no-store
时,不允许浏览器和所有中间层缓存response
。。例如:一些类似于银行及其他隐私数据不适合缓存,每次的用户请求都需要发送到服务端,下载全部的response。
2.2 “public” 和 “private”
public
如果response被标记为“public
”,那么,即使有与其相关的HTTP认证信息或者返回的response是不可缓存的status code,它依然可以被缓存。大多数情况下,public
并不是必需的,因为其他具体指示缓存的信息,如max-age
会表明当前的response在任何情况下都是要缓存的。
private
相比之下,浏览器可以缓存private
的response。但是,这些响应通常只用于单个用户,因此不允许其他中间缓存对齐进行缓存。例如:一个用户的浏览器可以带有用户私有信息的HTML页面,但是CDN无法缓存页面。
2.3 max-age
max-age
指令指定了允许重用缓存的response的最长时间(以秒为单位)。例如,max-age=60
表示response可以缓存,并且在接下来的60s内可以被重用,无需发出新的request请求。
三、 定义最佳的Cache-Control策略
遵循上面的流程图为应用程序中使用的特定资源或一组资源制定最佳缓存策略。理想的情况下,我们应该在客户端上在尽可能长的时间中缓存尽可能多的响应,并为每个响应提供验证令牌(ETag
),从而实现有效的重新验证。
Cache-Control指令和解释
指令 | 解释 |
---|---|
max-age=86400 | Response can be cached by browser and any intermediary caches (that is, it’s “public”) for up to 1 day (60 seconds x 60 minutes x 24 hours). |
private, max-age=600 | Response can be cached by the client’s browser only for up to 10 minutes (60 seconds x 10 minutes). |
no-store | Response is not allowed to be cached and must be fetched in full on every request. |
根据HTTP档案,排名前300,000(按照Alexa)的网站中,浏览器可以缓存几乎一半的下载响应,这对于网页的重复浏览和访问来说是个巨大的节省。当然,这并不意味着你的应用程序可以缓存50%的资源。有些网站的静态资源几乎不会变动,可能可以缓存超过90%的资源;其他网站可能有很多私有的或者是时间敏感的数据完全不能使用缓存。
审核自己的页面,确认哪些资源是可以缓存的并且确保它们返回了合适的Cache-Control
和Etag
。
四、作废和更新缓存的response
- 本地缓存的response可以一直被使用,直到资源
expires
; - 在URL中嵌入一个文件内容的指纹可以强制客户端不使用缓存,更新
response
; - 每个应用程序都需要定义自己的缓存层次结构来获得最佳性能。
所有从浏览器发出去的请求首先要路由到浏览器缓存中,检验是否存在可以用来完成该请求的有效缓存。如果有匹配的缓存,则response
从缓存中读取,从而消除网络延迟和传输引起的数据成本。
如何作废或者更新一个缓存的response
?
例如: 你已经告知访问者缓存一个css
样式表24h(max-age = 86400
),但是开发者刚刚提交了你希望向所有用户提供的更新。这时该如何通知访问者更新原有的缓存呢?此时,如果不改变资源的URL,无法更新资源。因为浏览器认为当前的缓存尚未过期。
浏览器缓存响应之后,直到根据max-age
或者expire
指示,缓存已过期或者缓存被清理调,都会使用缓存的资源。因此,在页面构建时,或者说在访问某个网站时,不同的用户可能使用不同版本的资源。刚刚获取资源的用户使用的是最新版本,但缓存了早期资源(仍然有效
)的用户依然使用旧版本的资源。
如何充分利用“客户端缓存”和“快速更新”?
在资源内容更改时,改变资源的URL,强制用户下载新的response
。通常,我们可以通过在文件名中嵌入版本号或者其他文件指纹来实现改变URL,例如:style.x234dff
。
定义每个资源的缓存策略(定义缓存结构层次),不仅可以控制每个资源的缓存时长,还可以控制访问者获取和查看新版本的速度。
如上图中的例子:
- HTML文件被标记为
no-cache
,意味着浏览器对于每个请求都会重新验证文档,如果资源内容发生改变,就会拉取最新版本。 此外,在HTML标记中,在css和javascript资源的URL中嵌入了指纹:如果这些文件的内容发生更改,HTML文件也会更改,从而加载一个HTML 响应的新副本。 - CSS允许浏览器和其他中间缓存(例如:CDN)进行缓存,过期时间为1年。其实,我们也可以使用
far future expires
of 1 year, 因为我们在文件名中嵌入了文件指纹,如果CSS文件发生更新,其请求的URL也会发生改变。 - js文件的过期时间也设置为1年,但是标记为
private
,可能是因为它里面包含了一些CDN不应缓存的私有用户数据。 - 图片文件中没有加版本和独一无二的hash指纹,直接进行缓存。缓存时间设置为1天。
结合ETag
、Cache-Control
和独一无二的URL
,可以实现最好的缓存策略:更长的过期时间、控制特定资源的缓存以及缓存位置、按需更新。
五、缓存设置清单
世界上没有最好的缓存策略。根据你的流量模式、所提供的数据类型以及应用程序对于资源新鲜度的特定需求,我们需要对每个资源以及整体的“缓存层次结构”制定适当的缓存策略。
制定缓存策略时的一些提示和技巧:
-
使用一致的URL(
hash/version
):如果对于内容完全相同的资源使用不同的URL,那么这个资源会不停地被获取和存储。Tips:URL需要区分大小写。
-
确保服务端提供了验证令牌(
ETag
): 验证令牌的存在避免了在服务端资源没有任何修改时传输完全相同的字节内容。 -
确定哪些资源可以被中介缓存(
public/private
):对于所有用户都相同的response可以在CDN和其他中介缓存中缓存。 -
确定每个资源的最佳缓存时长(
max-age
):不同的资源可能具有不同的新鲜度要求。审核并确认每个资源合适的max-age
。 -
确定当前网站的最佳缓存层次结构:结合资源的
URL
和资源内容的指纹以及HTML的短缓存或者no-cache
, 我们可以控制客户端获取和更新资源的速度。 - 减少混乱:有些资源的更新频率要高于其他资源。如果有一部分资源(比如:一个javascript函数或者一组CSS样式)需要经常更新,可以考虑将这一部分代码作为单独的文件。这样做,其余部分的资源(例如:不经常更新的库代码)就可以从缓存中提取。当获取更新时,使得需要下载内容的数量最小化