管理Web缓存的最常用和最有效的方法之一是通过Cache-Control HTTP标头,由于此标头适用于Web页面的缓存,这意味着我们页面上的所有内容都可以具有非常精细化的缓存策略。通过各种自定义策略,我们控制的策略就可以变得非常复杂和强大。
Cache-Control
管理Web缓存的最常用和最有效的方法之一是通过Cache-Control HTTP标头,由于此标头适用于Web页面的缓存,这意味着我们页面上的所有内容都可以具有非常精细化的缓存策略。通过各种自定义策略,我们控制的策略就可以变得非常复杂和强大。
Cache-Control标头可能如下所示:
- Cache-Control: public, max-age=31536000
Cache-Control是标头,public和max-age=31536000都是指令。 Cache-Control标头可以接受一个或多个指令,我想在本文中讨论的就是这些指令,比如它们的真正含义以及它们的***用例。
public和private缓存
public意味着任何缓存都可以存储响应的副本,其中就包括CDN、代理服务器等。public指令通常是多余的,因为其他指令(比如max-age)的存在是隐式指令,缓存可能会存储一个副本。
另一方面,private是一个显式指令,只有响应的最终接收者(客户端或浏览器)才可以存储该文件的副本。虽然private本身不是具有安全功能,但是它的目的是防止public缓存(例如CDN)存储包含一个用户唯一信息的响应。
max-age
max-age定义了一个以秒为单位的时间单位(相对于请求的时间),该单位的响应被认为是‘fresh’。
- Cache-Control: max-age=60
此Cache-Control标头会告诉浏览器,它可以在接下来的60秒内使用缓存中的此文件,而不必担心重新被验证。 不过60秒后,浏览器将返回服务器以重新验证文件。
如果服务器有一个新文件供浏览器下载,它将以200响应进行响应,下载新文件后,旧文件将从HTTP缓存中弹出,新文件将替换它,并将成为新的缓存标头。
如果服务器没有需要下载的更新副本,则服务器将以200响应进行响应,不需要下载任何新文件,并将使用新的标头更新缓存副本。这意味着,如果仍然存在Cache-Control:max-age = 60标头,则缓存文件在60秒后将再次启动。算下来,一个文件的总缓存时间为120秒。
注意:max-age会有自动警告的属性,如果浏览器过于陈旧,则max-age会提醒用户,但用户可以选择忽略此警告。浏览器可能会使用自己的试探法来决定是否在不重新验证文件的情况下发布文件的陈旧副本。这种行为有些不确定,所以很难确切地知道浏览器将实际做什么。为此,我们有一系列显式指令,可以用它们来扩充max-age。
s-maxage
s-maxage将优先于max-age指令,但仅限在共享缓存的上下文中使用。将max-age和s-maxage结合使用,你可以分别为private和public缓存(例如代理、CDN)提供不同的启动时间。
no-store
- Cache-Control: no-store
如果我们不想缓存文件怎么办?如果文件包含敏感信息怎么办?也许这是一个包含银行详细信息的HTML页面?或许这些信息对时间至关重要?也许是一个包含实时股票价格的页面?其实我们并不想在缓存中存储或提供任何类似的响应:我们总是希望丢弃敏感信息并获取***的实时信息。这时,我们就要用到no-store指令。
no-store是一个非常强大的指令,不会将任何信息保存到任何缓存中,无论是private或其他缓存。
no–cache
- Cache-Control: no-cache
这是让大多数人误解的指令,no – cache存并不意味着“没有缓存”。这只是意味着“在你使用服务器重新验证缓存之前,不需要从缓存中提供副本就可以使用以前的缓存副本”。
no-cache实际上是一种非常聪明的缓存更新策略,这样就可以始终保证***的缓存副本。除非服务器响应更快,否则no-cache将始终must-revalidate服务器才能释放浏览器的缓存副本,但如果服务器响应速度一般,网络传输只有一个文件的标头,则可以直接从缓存中抓取正文而不是重载。
因此,这是一种结合更新策略,并快速从缓存中获取文件的智能方法,但前提是它至少要获得一个HTTP标头响应。
无缓存的一个很好的用例几乎就是动态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指令,如上所示,我们把它设置为十分钟。此时,no-cache将立即与服务器重新验证,只有在服务器允许时才使用缓存副本时,must-revalidate才类似于一个宽限期no-cache。具体过程是这样的,在前十分钟,浏览器不会与服务器重新验证,但十分钟过后,它又返回服务器。如果服务器没有任何新内容,它将以304响应并且新的Cache-Control标头应用于缓存文件。然后再以十分钟为单位,如果在十分钟之后,服务器上有一个较新的文件,我们会收到200响应及其正文,并且本地缓存会更新。
proxy-revalidate
与s-maxage类似,proxy-revalidate是must-revalidate的public缓存的自定义版本,它只是被private缓存忽略了。
immutable
immutable是一个非常新的且非常简洁的指令,它告诉浏览器关于我们发送的文件类型的更多信息,该指令可以解决以下问题:用户刷新会导致浏览器重新验证文件,无论其新鲜度如何,用户刷新通常意味着以下任意情况必定发生:页面看起来不完整或者内容还和原来一样。
所以,我们有必要检查服务器上是否有更新的内容。
如果服务器上有更新的文件,我们肯定希望下载它。因此,我们将得到200响应,即一个新文件出现。但是,如果服务器上没有新文件,我们将得到304响应,即没有新文件,如果是专业,则整个延迟反应就没有意义了。如果我们重新验证许多导致延迟反应304的文件,可能会增加数百毫秒不必要的等待。
immutable是一种告诉浏览器文件有无可变内容的指令,如果内容无更新,则永远不会重新验证缓存。这样,就可以完全消除延迟时间。不过,immutable所指的可变或不可变文件的具体含义是什么?
style.css:当我们更改此文件的内容时,即使根本不更改其名称,这个文件也被认为是可变的。
style.ae3f66.css:这个文件是唯一的,它以基于其具体内容来命名的,所以一旦内容发生变化,我们就会得到一个全新的文件,此时,这个文件就被认为是不可变的。
我们将在Cache Busting部分中更详细地讨论这个问题。
如果我们能够以某种方式向浏览器发出文件是不可变的信号,则它就不需要检查更新版本,这正是immutable指令的作用:
Cache-Control: max-age=31536000, immutable
在支持immutable指令的浏览器中,用户刷新永远不会在31536000秒的新鲜度生命周期内进行重新验证。这意味着无需花费不必要的延迟时间来检索304响应,这可能会在关键路径(CSS blocks rendering)上节省大量的延迟时间。
注意:你不应该将immutable应用于任何不可变的文件,因为你还应该有一个非常强大的缓存破坏策略。
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天(2419200秒)以内的缓存文件都是新的,如果我们在那之后遇到更新错误,我们会多追加一天(86400秒),在此期间我们将允许过时的资源响应。
Cache Busting(缓存破坏)
只讨论缓存得正常情况而不讨论缓存破坏的情况是不负责任的。在考虑你的缓存策略之前,我总是建议你解决缓存破坏策略。因为当开发者修改了网站就会发生问题,因为用户本地缓存的文件还是老文件。这样用户看到的不仅还是旧的功能,如果网站缓存了css和js文件,它们还在引用不存在的元素或者被移除的被重命名的元素,网站就会报错破坏。
Cache busting就是强制浏览器下载新文件的一种方法,通过将新文件的名字修改成和旧文件不同的名字即可实现。
No Cache Busting – style.css
这是最不可取的做法:绝对没有任何缓存破坏。这是一个可变文件,我们真的很难实现Cache Busting。
你应该非常谨慎地缓存这些文件,因为一旦它们出现在用户的设备上,我们几乎失去了对它们的所有控制权。
尽管这个例子是一个样式表,但HTML页面正好就是这个特性。由于我们无法更改网页的文件名,网站就会报错破坏!这正是我们根本不会缓存它们的原因。
Query String – style.css?v=1.2.14
此时,我们仍然有一个可变文件,但我们在其文件路径中添加了一个查询字符串。虽然这比什么都不做要好,但它仍然不***。如果要删除查询字符串,我们会回到之前的类别,即完全不存在缓存破坏。许多代理服务器和CDN都不会通过配置来缓存任何带有查询字符串的内容。例如,来自Cloudflare的文档, “style.css?something”的请求将被标准化为“style.css”,或查询字符串可能包含特定于一个特定响应的信息。
Fingerprint – style.ae3f66.css
到目前为止,指纹识别是缓存破坏文件的***方法。每次文件的内容发生变化时,我们都对其进行更改,这并不会缓存任何内容。这意味着,我们将最终得到的是一个全新的文件!且该文件不可更改。如果你可以在静态缓存上实现此功能,请执行此操作!一旦你成功地实现了这个非常可靠的缓存破坏策略,你就能得到***的缓存控制指令了:
- Cache-Control: max-age=31536000, immutable
实施细节
此方法的关键是更改文件名,但不一定是对指纹进行更改。以下所有示例都具有相同的效果:
- /assets/style.ae3f66.css:使用文件内容的哈希产生破坏;
- /assets/style.1.2.14.css:使用一个已发布的版本产生破坏;
- /assets/1.2.14/style.css:通过更改URL中的目录产生破坏;
但是,***一个示例表明,我们对每个版本而不是每个单独的文件进行版本控制。这反过来意味着,如果我们只需要缓存样式表,则还必须缓存该版本的所有静态文件,所以***选项是前两个。
Clear-Site-Data
目前我们正在开发一个规范,以帮助开发人员确定整个缓存的来源,并从根上彻底一次性清除,这就是Clear-Site-Data的含义,它让Web开发人员对浏览器本地存储的数据有更多控制能力。
我不想在这篇文章中详细介绍Clear-Site-Data,它不是Cache-Control指令,而是一个全新的HTTP标头文件。
- Clear-Site-Data: "cache"
将此标头应用于你的任何一个源缓存都将让整个源的缓存破坏,而不仅仅是它所附加的文件。这意味着,如果你需要从所有访问者的缓存中强制破坏整个站点,则可以将上述标头应用于HTML有效内容。
在撰写本文时,仅支持浏览器有Chrome,Android Webview,Firefox和Opera。
提示:Clear-Site-Data将接受许多指令:“cookies”,“storage”,“executionContexts”和“*” (当然,“*”的意思是“以上所有”)。
具体示例
好的,让我们来看看一些应用场景以及我们可能采用哪种Cache-Control标头。
网上银行页面
- Request URL: /account/
- Cache-Control: no-store
根据规范,这足以阻止浏览器在private和共享缓存中持续对磁盘的响应。
no-store响应指令会命令缓存不得存储立即请求或响应的任何部分,此指令适用于private和共享缓存。 “不得存储”意味着缓存不得故意将信息存储在非易失性存储中,并且必须尽***努力尽快在转发后从易失性存储中删除所存储的信息。
但如果你想要防止缓存的发生,你可以选择:
- Request URL: /account/
- Cache-Control: private, no-cache, no-store
该指令将明确指示不要在public缓存(例如CDN)中存储任何内容,以始终提供***的副本。
列车时间表网页
如果我们正在构建一个显示实时信息的页面,则希望保证用户始终能够看到***的信息。此时,可以使用以下指令:
- Request URL: /live-updates/
- Cache-Control: no-cache
这个简单的指令意味着浏览器不会直接从缓存中显示响应,意味着页面不会显示过时的列车信息。
FAQ页面
像FAQ这样的页面可能很少更新,因为其中的内容大多都是常识性问题,对时效性没有要求。我们可能会暂时缓存这样的HTML页面,并强制浏览器定期检查新内容,而不是每次对缓存进行访问。可以使用以下指令:
- Request URL: /faqs/
- Cache-Control: max-age=604800, must-revalidate
该指令会告诉浏览器将HTML页面缓存一周(604800秒),并且一周结束后,我们需要检查服务器是否有更新。
页面中的图片
页面中的图片通常都是一篇文章的配图,通常我们都会下载下来,所以我们想缓存它。但其实它对页面的更新状态并不会产生影响,因此我们不需要它的更新状态。可以使用以下指令:
- Request URL: /content/masthead.jpg
- Cache-Control: max-age=2419200, must-revalidate, stale-while-revalidate=86400
该指令会告诉浏览器将图像存储28天(2419200秒),我们要在28天的时间限制后检查该图像在服务器中是否有更新。
总结
1.判断是否设置了cacheBusting属性非常重要。在开始执行缓存策略之前,请先制定缓存破坏策略。
2.一般来说,缓存HTML内容是一个错误的方法。 由于HTML网址不能被破坏,并且由于你的HTML页面通常是其余子资源的入口点,因此你最终也会缓存对静态资源的引用。
3.如果要缓存任何HTML,在站点上的不同类型的HTML页面上使用不同的缓存策略可能会导致不一致,比如有的页面总是***的内容,而其他页面的内容有时是从缓存中获取的。
4.如果你能够可靠的缓存(使用指纹)的静态资产,那么你还不如一次性使用一个不可变的指令缓存数年,以便更好地进行管理。
5.非关键缓存内容可以使用stale-while-revalidate等指令,增加缓存的宽限期。
6.immutable和stale-while-revalidate不仅为我们提供了缓存的传统优势,而且还允许我们在重新验证时降低延迟。
7.充分了解你的缓存,并设计具有针对性的缓存策略。