Cache Control for Civilians
第一篇译文
这是一篇译文,原文在这里,英文好的可以去阅读原文,已获得作者同意翻译本文。 第一次翻译这么长的,如有错误,请指出,一定修改。
文章目录
最好的网络请求就是永远都不会发生的请求: 想要获得更快的网站时,避免网络请求会远胜于有网络。因此,拥有一个可靠的缓存策略可以让你的用户感觉非常不一样。
话虽如此,但在我的工作中,越来越多的时候我看到是不考虑甚至完全忽视缓存实践。也许是因为第一次看见时没有过度关注,或者可能是因为缺乏这样的意识或知识?不管怎样,让我们来复习一下。
Cache-Control
一个最普遍、有效的管理资源缓存的方法是通过HTTP的头部字段Cache-Control
,这个头部可以应用到每一个网站的资源,这意味着我们页面上的所有内容都可以有一个非常定制和精细的缓存策略。这也使得其非常复杂和强大。
一个Cache-Control
头部可能看起来像这样:
Cache-Control: public, max-age=31536000
Cache-Control
是字段名,public
和max-age=31536000
是指令。Cache-Control
可以接受多个指令,在这篇博客中我会介绍这些指令以及它们的用例
public and private
public
意味着任何可以缓存的地方都会缓存一份响应的数据。包括CDN,代理服务器等。public
通常都是多余的,因为其他的指令(比如max-age
)已经隐含它了。
相反,private
指令只允许在相应的接受方(客户端或浏览器)保存一份。虽然private
不是一个安全相关的指令,但是它可以防止公共缓存(比如CDN)缓存一些用户相关的信息
max-age
max-age
定义了一段时间(相对于请求时)来告诉浏览器相应是否需要刷新
Cache-Control: max-age=60
这条指令会告诉浏览器,它可以在接下来的60秒内使用缓存中的这个文件,而不必重新验证它。60秒后,浏览器将服务器发请求重新验证文件。
如果服务器有一个新文件供浏览器下载,它会返回状态码200,下载新文件,并替换掉HTTP缓存中旧文件,并按照指令缓存文件。
如果服务器没有需要下载的更新副本,服务器会以304响应,并更新缓存的时间。这意味着,如果Cache-Control: max-age=60
仍然存在,缓存文件还将缓存60秒。总共缓存120秒。
注意: 如果单单使用max-age
会有一个非常大的风险…max-age
可以告诉浏览器资源是否已经过时,但是它不能告诉浏览器它是否可以使用过时的版本。浏览器可能在没有验证的情况下使用过期的版本。这种行为有些不确定性,导致我们很难判断浏览器究竟做了什么。幸好,我们有一系列更明确的指令,我们可以用这些指令来增强max-age
。感谢Andy Davies
的帮助
s-maxage
The s-maxage (note the absence of the - between max and age) will take precedence over the max-age directive but only in the context of shared caches. Using max-age and s-maxage in conjunction allows you to have different fresh durations for private and public caches (e.g. proxies, CDNs) respectively.
s-maxage
(注意没有"-“在max
和age
之间)将优先于max-age
指令,但仅在共享缓存的情况下有效。结合使用max-age
和s-maxage
,你可以分别对私有缓存和公共缓存(例如代理、CDN)拥有不同的缓存刷新时间。
no-store
Cache-Control: no-store
如果我们不想缓存文件怎么办?如果文件包含敏感信息怎么办?也许这是一个包含你银行详细信息的HTML页面?又或者信息是实时的?也又可能是一个包含实时股票价格的页面?我们肯定不想缓存或从缓存中获取这样的信息: 我们总是希望清除敏感的信息,然后获取最新的实时信息。现在我们需要用no-store
。
no-store
是一条非常强大的指令,告诉浏览器或其他设备不要缓存任何信息。任何带有这条指令的资源,无论在什么情况下都会发起请求。
no-cache
Cache-Control: no-cache
这个是让大多数人困惑的。no-cache
并不意味着没有缓存。它的意思是在你向服务器重新验证缓存副本,并且服务器表示你可以使用缓存副本之前,不要使用缓存。是的,这听起来应该叫做必须重新验证!只是听起来也不是这样。
no-cache
实际上是一种非常聪明的方式,可以保证内容始终是最新的,但是如果可能的话,也可以使用更快的缓存策略。no-cache
总是会发起网络请求,因为在返回浏览器的缓存之前,它必须与服务器重新验证(除非服务器有更新的响应内容),但是如果服务器的响应良好,网络传输只是文件的头部:可以从缓存中抓取主体,而不是重新加载。
所以,就像我说的,这是一种将获取最新资源和从缓存中获取文件的可能性结合起来的聪明方法,但是它会发出网络请求,至少会有HTTP头响应。
一个好的no-cache
用例几乎是任何动态HTML页面。想想一个网站的首页:它不是实时的,也不包含任何敏感信息,但是理想情况下,我们希望页面总是显示最新的内容。我们可以使用cache-control: no-cache
来指示浏览器首先检查服务器,如果服务器没有更新的内容( 304 ),我们就使用缓存版本。如果服务器确实有一些更新的内容,它会照原样做出响应( 200 )并发送更新的文件。
提示:在发送no-cache
指令的同时发送max-age
指令是没有用的,因为重新验证的时间限制是零秒。
must-revalidate
更令人困惑的是,虽然上面的那条指令听起来应该被称为must-revalidate
,但事实上must-revalidate
是不同的(但相似)。
Cache-Control: must-revalidate, max-age=600
must-revalidate
需要相关的max-age
指令;上面代码中,我们把它设置为十分钟。
其中no-cache
将立即与服务器进行重新验证,并且只有在服务器认为可以的情况下才使用缓存,must-revalidate
就像有宽限期的no-cache
一样。在最初的十分钟里,浏览器不会发请求去服务器重新验证,但是十分钟过后,就会发请求去服务器验证了。如果服务器说没有什么新的,它会以304作为响应,并且新的缓存控制头会应用到缓存中—十分钟后才会再次发送请求。如果十分钟后,服务器上有一个更新的文件,我们会得到一个200的响应和它的内容,并且本地缓存会得到更新。
must-revalidate
的一个很好的例子就是像我这样的博客:有很少改变的静态页面。当然,最新的内容是可取的,但是考虑到我的网站变化的频率,我们不需要像no-cache
一样频繁的检测更新。十分钟验证一次足够了。
proxy-revalidate
与s-maxage类似,proxy-revalidate
是must-revalidate
的一个只对公共缓存起作用的特定版本。
immutable
immutable
一个非常新且简洁的指令,它告诉浏览器更多关于我们发送的文件的类型——它的内容是可变的还是不可变的?但是,在我们看immutable
怎么用之前,让我们先看看它正在解决的问题:
用户刷新会导致浏览器重新验证文件,而不管文件是否是最新的,因为用户刷新通常意味着以下两种情况之一:
- 页面不能工作了,或者
- 内容过时了
那么让我们检查一下服务器上是否还有更新的东西。
如果服务器上有更新的文件,我们肯定想下载它。因此,我们将得到200个响应、一个新文件,并且——希望——问题得到解决。然而,如果服务器上没有新文件,我们将返回一个304头部,没有新文件,而是一个完整的往返延迟。如果我们重新验证许多文件,导致很多的304,这可能会增加数百毫秒的不必要的开销。
immutable
是一种告诉浏览器文件永远不会改变的方式,它是不可变的,因此永远不需要重新验证它。我们可以完全减少往返延迟的开销。我们所说的可变或不可变文件是什么意思呢?
- style.css: 当我们改变这个文件的内容时,我们根本不改变它的名字。文件总是存在的,它的内容总是变化的。这个文件是可变的。
- style.ae3f66.css: 这个文件是独一无二的——它是以基于内容的指纹命名的,所以当内容改变的时候,我们得到了一个全新的文件。这个文件是不可变的。
我们将在缓存刷新部分详细讨论这一点。
如果我们能够以某种方式向浏览器传达我们的文件是不可变的——它的内容永远不会改变——那么我们也可以让浏览器知道,它不需要费心去检查更新的版本:永远不会有更新的版本,因为当文件的内容改变时,它就不再存在了。
这正是不可变指令的作用:
Cache-Control: max-age=31536000, immutable
在支持immutable
的浏览器中,用户刷新永远不会在31536000秒内导致重新验证。这意味着不会花费不必要的往返时间在304响应上,这可能会在关键路径上为我们节省大量延迟( CSS块渲染)。在高延迟连接上,这种节省可能是显而易见的。
注意: 你不应该把immutable
应用到任何不是不可变的文件。您还应该有一个非常强大的缓存刷新策略,这样您就不会无意中主动缓存一个应用了不可变的文件。
stale-while-revalidate
我真的,真的希望stale-while-revalidate
能得到有更好的支持。
到目前为止,我们已经谈了很多关于重新验证的事情: 浏览器发请求到服务器检查是否有文件更新。在高延迟连接上,仅重新验证的持续时间就已很明显,并且该时间就是死时间——在我们从服务器得到响应之前,我们既不能释放缓存(304),也不能下载新文件(200)。
stale-while-revalidate
提供了一个宽限期(由我们定义),在宽限期内,当我们检查更新版本时,允许浏览器使用过期资源。
Cache-Control: max-age=31536000, stale-while-revalidate=86400
这条指令告诉浏览器,“这个文件可以使用一年,但是在那一年结束后,你有一个额外的星期可以继续使用这个过时的资源,同时在后台重新验证它”。
对非关键资源来说,stale-while-revalidate
是一条很好的指令,当然,我们想要最新的版本,但是我们知道,如果我们在检查更新时使用过期的资源,不会造成任何损害。
stale-if-error
以类似于tale-while-revalidate
的方式,stale-if-error
允许浏览器有一个宽限期,在该宽限期内,如果重新验证的资源返回5xx类错误,你可以返回一个过期的资源。
Cache-Control: max-age=2419200, stale-if-error=86400
这条指令,我们指示缓存文件在28天(2419200秒)内是新的,并且如果在此之后遇到错误,我们将允许额外的一天(86400秒),在此期间我们返回过期资源。
Cache Busting
谈论缓存而不谈论缓存刷新是不负责任的。我总是建议在考虑缓存策略之前先解决缓存刷新策略。反之则非常头痛。
缓存刷新是为了解决这样一个问题: 我刚刚告诉浏览器在下一年里使用这个文件,但是我刚刚改变了它,我不想让用户等一整年后才得到新的文件!我该怎么能干预呢?!
没有缓存刷新 – style.css
这是最不可取的做法: 这是绝对不会刷行缓存的。这是一个可变文件,我们很难缓存它。
您应该非常小心地缓存这样的文件,因为一旦它们出现在用户的设备上,我们几乎就失去了对它们的所有控制。
尽管这个例子是一个样式表,但是HTML页面也完全属于这个阵营。我们不能改变网页的文件名——想象一下这会造成多大的影响!—这就是为什么我们倾向于根本不缓存它们。
请求参数 – style.css?v=1.2.14
这里,我们仍然有一个可变的文件,但是我们在它的文件路径中添加了一个查询字符串。虽然比什么都不加要好,但它仍然不完美。如果在中间的什么步骤去掉了查询字符串,我们就可以回到之前的情况,即完全没有缓存刷新。许多代理服务器和CDN不会缓存带有字符串的任何内容(例如,从Cloudflare自己的文档就说过对"style.css?something"的请求会被替换为"style.css”),可能是配置,也可能是处于防御性地目的(查询字符串可能包含特定于一个特定响应的信息)。
文件名唯一 – style.ae3f66.css
指纹识别是缓存刷新文件的首选方法。通过每次内容改变时都改变文件名,我们在技术上不会缓存任何东西:我们最终会得到一个全新的文件!这非常健壮,并且允许使用不可变的。如果您可以在静态资产上实现这一点,请这样做!一旦您成功实现了这种非常可靠的缓存刷新策略,您就可以使用最激进的缓存形式:
Cache-Control: max-age=31536000, immutable
实现细节
这个方法的关键是改变文件名,但它不一定是指纹。以下所有示例都具有相同的效果:
- /assets/style.ae3f66.css: 通过hash文件来刷新.
- /assets/style.1.2.14.css: 通过版本号来刷新.
- /assets/1.2.14/style.css: 通过目录结构来刷新.
然而,最后一个例子意味着我们对每个版本进行版本控制,而不是对每个单独的文件进行版本控制。这反过来意味着,如果我们只需要缓存我们的样式表,我们还必须缓存该版本的所有静态文件。这是潜在浪费,所以最好选择(1)或(2)。
Clear-Site-Data
缓存失效很难——众所周知如此——所以目前有一个规范正在帮助开发人员非常明确地一次性清除他们站点的整个缓存:Clear-Site-Data
。
我不想在这篇文章中讲太多细节,因为Clear-Site-Data
不是缓存控制指令,而是一个全新的HTTP头。
Clear-Site-Data: "cache"
将该头应用到您的任何一个源资产将清除整个源的缓存,而不仅仅是它所附加的文件。这意味着,如果你需要从所有访问者的缓存中清除你的整个站点,你可以只将上面的标题应用到你的网页上。
在撰写本文时,浏览器支持仅Chrome, Android Webview, Firefox, 和 Opera。
提示: Clear-Site-Data将接受许多指令:“cookies”、“storage”、“executionContexts"和”*"(表示所有上述内容)。
例子和一些用法
好吧,让我们来看一些可能使用Cache-Control
头部的场景。
在线的银行页面
像网上银行应用程序页面,列出你最近的交易,当前的余额,也许还有些敏感的银行账户信息,需要是最新的(想象一下,你收到了一个列出你一周前余额的页面!)并确保不被他人看见(您肯定不希望您的银行详细信息存储在共享缓存(或者任何缓存,真的) )。
为此,我们可以使用:
Request URL: /account/
Cache-Control: no-store
根据规范,这足以防止浏览器在私有和共享缓存中长久的保存响应:
no-store
告诉缓存不准存储请求或响应的任何部分。该指令适用于私有和共享缓存。“不准"在这里意味着缓存不得有意将信息存储在非易失性存储器中,并且必须尽最大努力在转发信息后尽快将其从易失性存储器中移除。
但是如果你想有更强的防御性,也许你可以选择:
Request URL: /account/
Cache-Control: private, no-cache, no-store
这将明确指示不要在公共缓存(例如CDN )中存储任何内容,要始终提供尽可能最新的副本,并且不要将任何内容保存到存储中。
实时列车时刻表页面
如果我们正在构建一个显示近实时信息的页面,如果这些信息存在的话,我们希望确保用户总是能看到我们能给他们的最好的、最新的信息。我们可以使用:
Request URL: /live-updates/
Cache-Control: no-cache
这个简单的指令意味着浏览器在与服务器确认缓存是否被允许使用前不会直接从缓存中显示响应。这意味着用户永远不会看到过时的列车信息,但是如果服务器要求缓存镜像最新的信息,他们就可以从从缓存中获取。
对于几乎所有网页来说,这通常是一个明智的默认设置:给我们最新的可能内容,但是如果可能的话,让我们使用缓存来加速。
FAQ页面
像FAQ这样的页面很可能很少更新,而且其中的内容也不太可能对时间敏感。它当然没有实时运动成绩或飞行状态那么重要。我们可能可以缓存这样的页面一段时间,并强制浏览器定期检查新内容,而不是每次访问。让我们看看:
Request URL: /faqs/
Cache-Control: max-age=604800, must-revalidate
这告诉浏览器将网页缓存一周( 604800秒),一旦这一周结束,我们就需要向服务器查询是否需要更新。
注意:在同一个网站中对不同的页面采用不同的缓存策略可能会导致这样一个问题: 您的 no-cache
主页请求它引用的最新样式,f4fa2b.css,但是您的要缓存三天的FAQ页面仍然指向样式a3f66.css。这可能会有轻微的影响,但这是一个您应该注意的场景。
静态的js/css
假设我们的app.[fingerprint].js文件更新非常频繁——可能在我们发布的每一个版本中都是如此——但是我们也在每次文件发生变化时都会根据内容对其重命名(这非常好),然后我们可以这样做:
Request URL: /static/app.1be87a.js
Cache-Control: max-age=31536000, immutable
我们是否非常频繁地更新JS并不重要:因为我们有可靠地缓存刷新,所以我们可以想缓存多久就缓存多久。在这种情况下,我们选择缓存一年。我选择了一年,因为首先,一年是很长的时间,其次,浏览器实际上不太可能将文件保存那么长时间(浏览器有有限的存储空间可以用于HTTP缓存,所以它们自己会定期清空部分文件;用户可能也会清除他们缓存)。任何超过一年的设置不会更有效。
此外,因为这个文件的内容从不改变,我们可以让浏览器知道这个文件是不可变的。即使用户刷新了页面,我们也不需要在一年内对它进行重新验证。我们不仅获得了使用缓存的速度优势,还避免了重新验证的延迟损失。
装饰的图片
想象一张纯粹装饰性的照片伴随着一篇文章。它不是信息图或图表,它不包含任何对理解页面其余部分至关重要的内容,用户甚至不会真正注意到它是否完全丢失了。
图像通常是需要下载的非常重的资源,所以我们想缓存它;它对页面并不重要,所以我们不需要获取最新版本;我们甚至有可能在图像有点过时后就不再提供图像了。让我们这样做:
Request URL: /content/masthead.jpg
Cache-Control: max-age=2419200, must-revalidate, stale-while-revalidate=86400
在这里,我们告诉浏览器将图像存储28天( 2419200秒),我们要在28天的时间限制后向服务器查询更新,如果图像过期不到一周( 86400秒),我们在后台获取最新版本时使用该图像。
需要记住的
- 缓存刷新至关重要。在开始缓存策略之前,先制定缓存刷新策略。
- 一般来说,缓存网页内容是个坏主意。超文本链接不能被破坏,而且由于你的网页通常是进入你网页其余子资源的入口,你最终也会缓存对你静态资产的引用。这会让你(和你的用户)感到沮丧。
- 如果您要缓存任何一个网页,如果一个网站上不同类型的网页上有不同的缓存策略,并且其中一类网页总是新的,而另一类网页有时是从缓存中获取的,那么这可能会导致不一致。
- 如果你能依赖缓存刷新去缓存你的静态资源,那么你最好用一个不可变的指令一次性缓存多年。
- 非关键内容可以有一个过时的宽限期,指令类似于过期同时重新验证。
immutable
和stale-while-revalidate
不仅给我们带来了缓存的传统优势,还允许我们在重新验证时降低延迟成本。
尽量避免网络请求,可以让我们的用户获得更快的体验(同时降低基础设施的吞吐量)。通过对我们的资源有一个好的看法,并对我们可用的东西有一个概述,我们可以开始为我们自己的应用程序设计非常精细、定制和有效的缓存策略。
用缓存管理一切
相关资源
- Caching best practices & max-age gotchas - Jake Archibald, 2016
- Cache-Control: immutable – Patrick McManus, 2016
- Stale-While-Revalidate, Stale-If-Error Available Today – Steve Souders, 2014
- A Tale of Four Caches – Yoav Weiss, 2016
- Clear-Site-Data – MDN
- RFC 7234 – HTTP/1.1 Caching – 2014
按我说的做,别按我做的去做(作者调皮了)
在别人在Hacker News上喷我虚伪之前,我想说的是我自己的缓存策略非常不好,我甚至都不打算去做。