原文章

通过网络获取一些东西总是缓慢并且昂贵的。多数的响应需要在客户端和服务器之间往返多次。当它们可用并且浏览器能够处理它们的时候,就会发生延迟,还会访问者带来数据成本。因此,缓存和重用之前获取的资源的能力,是优化效率的一个至关重要的方面。

好消息是每个浏览器都附带一个HTTP缓存的实现。所有你需要做的事情,就是确保每个从服务器发出的响应,都提供了一个能够指示浏览器何时以及缓存该响应多长时间的有效的HTTP头部。

当服务器返回一个响应的时候,它也会发出一系列描述了它的内容类型、长度、缓存指令、令牌等的HTTP头部。例如,在上面的例子中,服务器返回了1024字节的响应,指示客户端最多缓存它120s,并且提供了一个可以被用来在响应过期之后,检查资源是否被更改的,值为”x234dff”的效验令牌。

通过 ETags 效验缓存的响应

摘要:

  • 服务器使用 ETag HTTP 头部传递一个效验令牌
  • 效验令牌支持高效的资源更新检查:如果资源没有改变,就没有数据会被传输。

假定自从最初的请求之后120s过去了,浏览器已经对相同资源发起了一个新请求。首先,浏览器检查本地缓存,会发现之前的响应。不幸的是,因为之前的响应现在已经过期了,浏览器并不能再使用它。在这个时候,浏览器可以发送一个新请求并且获取完整的新的响应。然而,这是低效率的。因为如果资源没有改变,那么就根本没有理由去下载已经存在于缓存中的相同信息!

这即是由 ETag 头部所指定的效验令牌被设计所用来解决的问题。服务器生成一个由文件内容的哈希或者其它指纹构建的任意令牌,并且将它返回。客户端不需要知道这个指纹是怎样生成的;只需要在下一次请求中将它发送给服务器。如果指纹仍然相同,表明资源没有被改变,那就可以跳过再次下载的步骤。

在之前的例子中,客户端自动在 “If-None-Match” HTTP 请求头部中提供了 ETag 令牌。服务器根据当前的资源检查令牌。如果令牌没有改变,服务器就返回 “304 Not Modified” 响应。这个响应告诉浏览器存在于缓存中的响应没有改变,可以再更新120s的使用时间。这样,你就不会重新下载响应,从而节省了时间和带宽。

作为一个 web 开发者,你应该怎样利用这个高效的重新验证方法?浏览器作为我们的代表完成所有的工作。如果验证令牌在之前被指定了的话,浏览器会自动检查,将验证令牌附加在发出的请求之上,并且根据从服务器接受的响应决定是否在必要的时候更新缓存的时间戳。唯一需要做的事情是确保服务器提供了必要的 ETag 令牌。你可以查看你服务器的文档以发现必要的配置。

Cache-Control

摘要:

  • 每个资源都可以通过 Cache-Control HTTP 头部定义它缓存的策略。
  • Cache-Control 指令控制了谁可以缓存响应、在什么条件之下可以缓存响应、以及缓存多少时间。

从优化效率的方面来说,最好的请求就是不需要与服务器沟通的请求:一个响应的本地副本支持你消除掉网络延迟,并且避免数据传输的费用。为了达到这一点,HTTP 规范支持服务器返回 Cache-Control 指令以控制浏览器和其它中间缓存服务器可以以怎样的方式和多长的时间缓存单个的响应。

“no-cache” 和 “no-store”

“no-cache” 指出了在没有初次与服务器检查资源是否修改的情况下,不能将之前返回的响应用来代替之后对同一 URL 的请求。结果是,如果一个正确的效验令牌(ETag)被设置了,为了验证缓存的响应是否有效,no-cache 将导致流量的往返。但如果资源没有被更改,就可以取消再次下载。

与之相比,”no-store” 要更简单。它只是禁止了浏览器和所有的中间缓存服务器储存任何版本的返回的响应——例如,一个包含了私人信息或者银行数据的响应。每次用户请求资源,一个新请求都会被发送给服务器,并且一份完整的响应将被下载。

“public” 和 “private”

如果一个响应被标记为”public”,那么它将可以被缓存,即使它被分配了一个 HTTP 认证,甚至是当响应的状态代码不是通常可缓存的时候。但大多数时候,”public”是没有必要的,因为任何像”max-age”这样的显示缓存信息都指示了响应可以被缓存。

与之相比,浏览器可以缓存”private”的响应。然而,这些响应通常是针对单个用户的,所以一个中间缓存服务器不被允许缓存它们。例如,一个用户的浏览器可缓存带有用户隐私信息的HTML页面,但是一个CDN不可以。

“max-age”

这个指令用以秒计算的方式,指定了从请求发起,获取的响应可以被重用的最大时间。例如,”max-age=60”指示了响应可以被缓存,并且可以在之后的60s里面被重用。

定义最优 Cache-Control 策略

按照上面的决策树决定你的浏览器可以为一个特别的或一系列的资源所用的最优缓存策略。理想情况下,你应该以在客户端上缓存尽可能多的响应,尽可能长的时间周期为目标,并且为以后的每次响应提供效验令牌以支持高效的重新验证。

根据HTTP Archive,在根据 Alexa 排名的前30万站点中,浏览器可以缓存所有下载响应中将近一半的信息,这是对重复页面访问和浏览的一个巨大节省。当然,这并不意味着你的特别的应用程序可以缓存50%以上的资源。因为一些站点可以缓存它们超过90%以上的资源,然而另一些站点带有大量私有或者时间敏感的不能全部缓存的数据。

审计你的页面以鉴别哪些资源是可以被缓存的,并且确保为它们返回了恰当的 Cache-Control 和 ETag 标签。

无效化以及更新缓存的响应

摘要:

  • 本地缓存将被使用到资源”过期”。
  • 在 URL 中嵌入一个文件内容指纹,支持你强制将客户端更新到一个新版本的响应。
  • 为了最优的缓存效率,每个应用程序都需要定义它自己的缓存层级。

所有由浏览器发出的 HTTP 请求,第一步都将路由到浏览器的缓存,检查这里是否有一个可以被用来满足请求的有效缓存响应。如果有匹配的,那么响应就可以从缓存里面读取,这消除了网络延迟,以及传输带来的数据成本。

然而,如果你想更新或无效化一个缓存的响应该怎么做呢?例如,假设你已经告诉你的访问者缓存一个CSS样式表最长24小时的时间(max-age=86400),但你的设计师刚刚提交了一个你想让所有用户的支持的更新。你怎样才能提醒所有现在拥有一个“变味”的CSS的缓存副本的用户,去更新他们的缓存呢?你不能,至少不改变资源的URL是不行的。

在浏览器缓存响应之后,直到缓存由 max-age 或者 expires 决定不再有效,或者是直到它因为某些其它原因从缓存之中被驱逐——例如,用户清空了浏览器的缓存——缓存的版本都将被使用。结果是,不同的用户在构建页面的时候,最终使用了不同的版本:刚刚获取资源的用户使用新版本,然而缓存了更早(但仍然有效)的副本的用户使用它响应的旧版本。

如何才能兼顾两者:客户端缓存和快速更新?你可以改变资源的 URL,并在它的内容改变时,强制用户下载新版本的响应。典型的,你通过在文件名里面嵌入文件的指纹,或者版本数字来实现这点——
例如,style.x234dff.css。

定义每个资源的缓存策略的能力,支持你定制“缓存层级“。它不单能够控制缓存多长时间,并且能够控制访问者能多快看到新版本的资源。为说明这一点,分析上述的例子:

  • HTML 页面被标记为”no-cache”,这意味浏览器总是在每次请求中重新验证,并且在内容改变的情况下获取最新资源。同样,在 HTML 标记里,你在 URL 里面为 CSS 和 JavaScript 嵌入了指纹:如果那些文件的内容改变了,HTML 页面同样将会改变,一个新 HTML 响应的副本将会被下载。
  • CSS 文件被支持由浏览器和中间缓存服务器(例如,一个 CDN)缓存,并且设定过期时间为一年。注意,你可以安全的使用超过一年的过期期限,因为你在它的文件名里面嵌入了文件指纹:如果 CSS 文件被更新了,那么它的 URL 也将会被改变。
  • JavaScript 文件同样设置了一年的过期时间,但被标记为私有,这可能是由于它包含了一些不该被 CDN 缓存的用户私有数据。

ETag,Cache-Control,以及唯一URL的联合使用允许你获得各个方面的最好实践:长过期时间,控制在哪里响应可以被缓存,以及按需更新。

Caching 清单

没有一种最好的缓存方针。你必须根据你的流量模式,服务的数据类型,以及应用指定的刷新数据的需要,来定义和配置合适的,针对每个资源的设置,以及整体上的”缓存层级“。

当你为缓存策略而工作时,记住以下一些提示和技巧:

  • 使用一致的 URL:如果你在不同 URL 上服务了相同内容,那么这些内容会被获取和储存多次。提示:记住 URL 是大小写敏感的。
  • 确保服务器提供了一个有效的效验令牌(ETag):当资源在服务器上没有被改变的情况下,效验令牌消除了传输相同字节的需要。
  • 标记哪些资源可以被中间机构缓存:那些对所有用户而言都相同的响应,是被 CDN 或其它中间机构缓存的优秀候选者。
  • 针对每个资源决定最优缓存生命期:不同的资源可能有不同的刷新需要。为每个资源审计并且决定适合的 max-age。
  • 定义你站点的最好缓存层级:联合使用带内容指纹的资源 URL以及具有很短缓存生命期或不缓存的 HTML 文档,能够使你控制客户端多快获取更新。
  • 最小化改变:一些资源比其它资源更新得更快。如果这儿有一个经常更新的资源(例如,一个 JavaScript函数或者一系列CSS样式表),考虑将代码作为单独的文件交付。这样做能够使其余的内容能够从缓存中获取(例如,库代码不会经常改变),并且在无论何时更新被获取时,使下载的内容总量最小化。