[译] 写给你们看的 Cache-Control 指令配置

最好的网络请求就是无须与服务器通讯的请求:在网站速度为王的比试里,避免网络完胜于使用网络。为此,使用一个可靠的缓存策略会给你的访客带来彻底不一样的体验。css

话虽如此,在工做中我愈来愈频繁地看到不少实践机会被无心识地错过,甚至彻底忽视作缓存这件事。大概是由于过分聚焦于首次访问,也可能单纯是由于意识和知识的匮乏。无论是为何,咱们有必要作一点相关知识的复习。html

Cache-Control

管理静态资源缓存最多见且有效的方式之一就是使用 Cache-Control HTTP 报头。这个报头独立应用于每个资源,这意味着咱们页面中的一切均可以拥有一个很是定制化、颗粒化的缓存政策。咱们由此能够获得大量的控制权,得以制定异常复杂而强大的缓存策略。前端

一个 Cache-Control 报头多是这样的:android

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

Cache-Control 就是报头字段名,publicmax-age=31536000指令Cache-Control 报头能够接受一个或多个指令,我在本文中想要讲的正是这些指令的真正含义和他们的最佳使用场景。ios

publicprivate

public 意味着包括 CDN、代理服务器之类的任何缓存均可以存储响应的副本。public 指令常常是冗余的,由于其余指令的存在(例如 max-age)已经隐式表示响应是能够缓存的。git

相比之下,private 是一个显式指令,表示只有响应的最终接收方(客户端或浏览器)能够缓存文件。虽然 private 自己并不具有安全功能,但它意在有效防止公共缓存(如 cdn)存储包含用户我的信息的响应。github

max-age

max-age 定义了一个确保响应被视为“新鲜”的时间单位(相对于请求时间,以秒计)。后端

Cache-Control: max-age=60
复制代码

可在接下来的 60 秒缓存和重用响应。浏览器

这个 Cache-Control 报头告诉浏览器能够在接下来的 60 秒内从缓存中使用这个文件而没必要担忧是否须要从新验证。60 秒后,浏览器将回访服务器以从新验证该文件。缓存

若是有了一个新文件供浏览器下载,服务器会返回 200,浏览器下载新文件,旧文件也会从 HTTP 缓存中被剔除,新的文件会接替它,并应用新缓存报头。

若是并无新的副本供下载,服务器会返回 304,不须要下载新文件,使用新的报头来更新缓存副本。也就是说若是 Cache-Control: max-age=60 报头依然存在,缓存文件的 60 秒会从新开始。这个文件的总缓存时间是 120 秒。

注意:max-age 自己有一个巨坑,它告诉浏览器相关资源已通过期,但没有告诉这个过时版本绝对不能使用。浏览器可能使用它本身的机制来决定是否在不经验证的状况下释放文件的过时副本。这种行为有些不肯定性,想确切知道浏览器会怎么作有点困难。为此,咱们有一系列更为明确的指令,用来加强 max-age,感谢 Andy Davies 帮我澄清了这一点。

s-maxage

s-maxage(注意 max 和 age 之间没有 -)会覆盖 max-age 指令,但只在公共缓存中生效。max-ages-maxage 结合使用可让你针对私有缓存和公共缓存(例如代理、CDN)分别设定不一样的刷新时间。

no-store

Cache-Control: no-store
复制代码

若是咱们不想缓存文件呢?若是文件包含敏感信息怎么办?好比一个包含你银行帐户信息的 HTML 页面,或者是有时效性的信息?再或者是个包含实时股价的页面?咱们根本不想从缓存中存储或释放响应:咱们想要的是丢掉敏感信息,获取最新的实时信息。这时候咱们须要使用 no-store

no-store 是一个很是高优先级的指令,表示不会将任何信息持久化到任何缓存中,不管是私有与否。任何带有 no-store 指令的资源都将始终命中网络,没有例外。

no-cache

Cache-Control: no-cache
复制代码

这点多数人都会困惑...... no-cache 并不意味着 “no cache”。它意味着“在你和服务器验证过而且服务器告诉你可使用缓存的副本以前,你能使用缓存中的副本”。没错,听起来应该叫 must-revalidate!不过其实也没听起来这么简单。

事实上 no-cache 一个能够确保内容最新鲜的很是智能的方式,同时也能够尽量使用更快的缓存副本。no-cache 老是会命中网络,由于在释放浏览器的缓存副本(除非服务器的响应的文件已更新)以前,它必须与服务器从新验证,不过若是服务器响应容许使用缓存副本,网络只会传输文件报头:文件主体能够从缓存中获取,而没必要从新下载。

因此如我所言,这是一个兼顾文件新鲜度与从缓存中获取文件可能性的智能方式,缺点是它至少会为了一个 HTTP 报头响应而触发网络。

no-cache 一个很好的使用场景就是动态 HTML 页面获取。想一想一个新闻网站的首页:既不是实时的,也不包含任何敏感信息,但理想状况下咱们但愿页面始终显示最新的内容。咱们可使用 cache-control: no-cache 来让浏览器首先回访服务器检查,若是服务器没有更新鲜的内容提供(304),那咱们就重用缓存的版本。若是服务器有更新鲜的内容,它会返回(200)而且发送最新的文件。

提示:max-age 指令和 no-cache 指令一块儿发送是没用的,由于从新验证的时间限制是零秒。

must-revalidate

更使人困惑的是,虽然上一个指令说应该叫 must-revalidate,但事实上 must-revalidate 依然是不一样的东西。(此次更相似一些)

Cache-Control: must-revalidate, max-age=600
复制代码

must-revalidate 须要一个关联的 max-age 指令;上文咱们把它设置为 10 分钟。

若是说 no-cache 会当即向服务器验证,通过容许后才能使用缓存的副本,那么 must-revalidate 更像是一个具备宽期限的 no-cache。状况是这样的,在最初的十分钟浏览器不会(我知道,我知道......)向服务器从新验证,可是就在十分钟过去的那一刻,它又到服务器去请求,若是服务器没什么新东西,它会返回 304 而且新的 Cache-Control 报头应用于缓存的文件 —— 咱们的十分钟再次开始。若是十分钟后服务器上有了一个新的文件,咱们会获得 200 的响应和它的报文,那么本地缓存就会被更新。

must-revalidate 一个很适合的场景就是博客(好比我这个博客):静态页面不多更改。固然,最新的内容是能够获取的,但考虑到个人网站不多更改,咱们不须要 no-cache 这么下重手的东西。相反,咱们会假设在十分钟内一切都好,以后再从新验证。

proxy-revalidate

s-maxage 一脉相承,proxy-revalidate 是公共缓存版的 must-revalidate。它被私有缓存简单地忽略掉了。

immutable

immutable 是一个很是新并且整洁的指令,它能够把更多有关咱们所送出文件类型的信息告知浏览器 —— 文件内容是可变或者不可变吗?了解 immutable 是什么以前,咱们先看看它要解决什么问题:

用户刷新会致使浏览器强制验证一个文件而不论文件新鲜与否,由于用户刷新每每意味着发生了这两件事之一:

  1. 页面崩溃之类的;
  2. 内容看起来已通过期了......

......因此咱们要检查一下服务器上是否有更加新鲜的内容。

若是服务器上有一个更新鲜的内容可用,咱们固然想下载它。这样咱们将获得一个 200 响应,一个新文件,而且 —— 但愿是 —— 问题已经修复了。而若是服务器上没有新文件,咱们将返回 304 报头,没有新文件,只有整个往返请求的延迟。若是咱们从新验证了大量文件且都返回 304,这会增长数百毫秒的没必要要开销。

immutable 就是一种告诉浏览器一个文件永远都不会改变的方法 —— 它是不可变的 —— 所以不要再费心从新验证它。咱们能够彻底减去形成延迟的往返开销。那咱们说的一个可变或不可变的文件是什么意思呢?

  • style.css:当咱们更改文件内容时,咱们不会更改其名称。这个文件始终存在,其内容始终能够更改。这个文件就是可变的。
  • style.ae3f66.css:这个文件是惟一的 —— 它的命名携带了基于文件内容的指纹,因此每当文件修改咱们都会获得一个全新的文件。这个文件就是不可变的。

咱们会在 Cache Busting 部分详细讨论这个问题。

若是咱们可以以某种方式告诉浏览器咱们的文件是不可变的 —— 文件内容永远不会改变 —— 那么咱们也可让浏览器知道它没必要检查更新版本:永远不会有新的版本,由于一旦内容改变,它就不存在了。

这正是 immutable 指令所作的事情:

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

在支持 immutable 的浏览器中,只要没超过 31,536,000 秒的新鲜寿命,用户刷新也不会形成从新验证。这意味着避免了响应 304 的往返请求,这可能会节约咱们在关键路径上(CSS blocks rendering)的大量延迟。在高延迟的场景里,这种节约是可感知的。

注意:千万不要给任何非不可变文件应用 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

stale-while-revalidate 相似的方式,若是从新验证资源时返回了 5xx 之类的错误,stale-if-error 会给浏览器一个使用旧的响应的宽限期。

Cache-Control: max-age=2419200, stale-if-error=86400
复制代码

这里咱们让缓存的有效期为 28 天(2,419,200 秒),事后若是咱们遇到内部错误就额外提供一天(86,400 秒),此间容许访问旧版本资源。

no-transform

no-transform 和存储、服务、从新验证新鲜度之间没有任何关系,但它会告诉中间代理不得对该资源进行任何更改或转换

中间代理更改响应的一个常见状况是电信提供商表明开发者用户作优化:电信提供商可能会经过他们的堆栈代理图片请求,而且在他们移动网络传递给最终用户前作一些优化。

这里的问题是开发人员开始失去对资源展示的控制,而电信服务商所作的图像优化可能过于激进甚至不可接受,或者可能咱们已经将图像优化到了理想程度,任何进一步的优化都不必。

这里,咱们是想要告诉中间商:不要转换咱们的内容。

Cache-Control: no-transform
复制代码

no-transform 能够与其余任何报头搭配使用,且不依赖其余指令独立运行。

小心:有的转换是很好的主意:CDN 为用户选择 Gzip 或 Brotli 编码,看是须要前者仍是可使用后者;图片转换服务自动转成 WebP 等。

小心:若是你是经过 HTTPS 运行,中间件和代理不管如何都不能改变你的数据,所以 no-transform 也就没用了。

Cache Busting

讲缓存而不讲缓存破坏(Cache Busting)是不负责任的。我老是建议甚至在考虑缓存策略以前就先要解决缓存破坏策略。反过来作就是自找麻烦了。

缓存破坏解决这样的问题:“我只是告诉过浏览器在接下来的一年使用这个文件,但后来我改动了它,我不想让用户拿到新副本以前要等一全年!我该怎么作?!”

无缓存破坏 —— style.css

这是最不建议作的事情:彻底没有任何缓存破坏。这是一个可变的文件,咱们真的很难破坏缓存。

缓存这样的文件你要很是谨慎,由于一旦在用户的设备上,咱们就几乎失去了对他们的全部控制。

尽管这个例子是一个样式表,HTML 页面也纯属这个阵营。咱们不能更改一个网页的文件名,想象一下这破坏力!—— 这正是咱们倾向于从不缓存它们的缘由。

查询字符串 —— style.css?v=1.2.14

这里依然是一个可变的文件,可是咱们在文件路径后加了个查询字符串。聊胜于无,但不尽完美。若是有什么东西把查询字符串删掉了,咱们就彻底回到了以前讲的没有缓存破坏的样子。不少代理服务器和 CDN 都不会缓存查询字符串,不管是经过配置(例如 Cloudflare 官方文档写到:“......从缓存服务请求时,‘style.css?something’将会被标准化成‘style.css’”)仍是防护性忽略(查询字符串可能包含请求特定响应的信息)。

指纹 —— style.ae3f66.css

添加指纹是目前破坏文件缓存的首选方法。每次内容变动,文件名都会随之修改,严格地讲咱们什么都不缓存:咱们拿到的是一个全新的文件!这很稳健,而且容许你使用 immutable。若是你能在你的静态资源上实现这个,那就去干!一旦你成功实现了这种很是可靠的缓存破坏策略,你就可使用最极致的缓存形式:

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

实施细节

这种方法的要点就是更改文件名,但它不非得是指纹。下面的例子都有一样的效果:

  1. /assets/style.ae3f66.css:经过文件内容的 hash 破坏。
  2. /assets/style.1.2.14.css:经过发行版本号破坏。
  3. /assets/1.2.14/style.css:改变 URL 中的目录。

然而,最后一个示例意味着咱们要对每一个版本进行版本控制,而不是独立文件。这反过来意味着若是咱们只想对咱们的样式表作缓存破坏,咱们也不得不破坏了这个版本的全部静态文件。这可能有点浪费,因此推荐选项(1)或(2)。

Clear-Site-Data

缓存很难失效 —— 这是闻名于计算机科学界的难题 —— 因而有了一个实现中的规范,这能够帮助开发者明确地一次性清理网站域的所有缓存:Clear-Site-Data

本文我不想深刻探究 Clear-Site-Data,毕竟它不是一种 Cache-Control 指令,事实上它是一个全新的 HTTP 报头。

Clear-Site-Data: "cache"
复制代码

给你的域下任何一个静态文件应用这个报头,就会清除整个域的缓存,而不只是它附着的这个文件。也就是说,若是你须要给你整个网站的全部访客的缓存来个大扫除,你只需把上面这个报头加到你的 HTML 上便可。

浏览器支持方面,截止到本文写做只支持 Chrome、Android Webview、Firefox 和 Opera。

提示:Clear-Site-Data 能够接收不少指令:"cookies""storage""executionContexts""*"(显然,意思是“上述所有”)。

栗子及其食用方法

Okay,让咱们看一些场景,以及咱们可能使用的 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 这样的页面可能不多更新,并且其内容不太可能对时间敏感。它固然没有实时运动成绩或航班状态那么重要。咱们能够将这样的 HTML 页面缓存一段时间,并强制浏览器按期检查新内容,而不用每次访问都检查。咱们这样设置:

Request URL: /faqs/
Cache-Control: max-age=604800, must-revalidate
复制代码

这会容许浏览器缓存 HTML 页面 一周时间(604,800 秒),一旦一周过去,咱们须要向服务器检查更新。

小心:给同一个网站的不一样页面应用不一样的缓存策略会形成一个问题,在你设置 no-cache 的首页会请求它引用的最新的 style.f4fa2b.css,而在你的加了三天缓存的 FAQ 页依然指向 style.ae3f66.css。这种状况可能影响不大,但不容忽视。

静态 JS(或 CSS)App Bundle

比方说们的 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 天(2,419,200 秒),28 天期限事后咱们想向服务器检查更新,若是图片没有超过一天(86,400 秒)的过时时间,那么咱们就在后台请求到最新版本后再替换它。

要牢记的要点

  • 缓存破坏极其极其极其重要。开始作缓存策略以前,先解决好缓存破坏策略。
  • 通常来讲,缓存 HTML 内容是个馊主意。HTML URL 不能被破坏,毕竟 HTML 页每每是访问页面其余子资源的入口点,你会把通往静态文件的引用声明也缓存下来。这会让你(和你的用户)......一言难尽。
  • 缓存 HTML 时,若是一类页面从不缓存而其余类页面有时要用缓存,这种同站不一样类型的 HTML 页的不一样缓存策略会致使不一致性。
  • 若是你可以给你的静态资源可靠地作缓存破坏(使用指纹),那你最好一次性把全部的东西都缓存好几年,以求最优。
  • 非关键内容能够用 stale-while-revalidate 之类的指令给一个不新鲜宽限期。
  • immutablestale-while-revalidate 不只能带来缓存的传统效益,还让咱们在从新验证时下降延迟成本。

尽量避免使用网络会为用户提供更快的体验(也会给咱们的基础设施更低的吞吐量,两开花)。经过对资源的详细了解和可用内容的总览,咱们能够开始针对咱们的应用设计作一个颗粒化、定制化且有效的缓存策略。

缓存在手,一切尽在掌控。

参考文献和相关阅读

依吾言行事,勿观吾行仿之

在某人因个人言行不类开喷以前,有必要一提的是我本身博客的缓存策略这么差强人意,以致于我本身都看不下去了。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索