网页缓存控制最佳实践

有时候,当你进入某个网站时它的样式会崩坏,但是刷新一下页面就好了。这通常是这个网站的缓存设置没有让你请求最新的样式文件。下面我将介绍如何正确的使用缓存设置,在你每次完成开发后让网站对所有的用户保持最新。

如果想直接看最佳实践,请点击该链接跳至文章结尾

缓存控制的工作原理

浏览器在每次请求的时候都会尝试通过读本地缓存的方法来请求尽可能少的数据。但这需要我们给浏览器提供一些指示,告诉它哪些资源需要缓存以及要缓存多久。

在 HTTP 协议中标准而通用的做法是把这些信息放在 HTTP 响应头中,最标准而普遍的做法是使用这些响应头关键字: Cache-ControlExpiresEtagLast-Modified

几乎所有的 web 服务器都会在响应时默认带上一些缓存控制的设置,但如果没有缓存策略,我们会得到什么结果还不清楚。

如果没有缓存控制设置,浏览器每次都要去请求服务器,并从中读取信息。这将延长网站的加载时间,对网络传输增加额外的负载,并增加对后端的调用次数。

在没有服务器指示的情况下是否对资源进行缓存则要取决于浏览器。当前,Chrome 和 Safari 默认每次都会请求后端。但这一行为可能会在未来变化,或者在其他平台上不同。

为了能清晰的告诉浏览器如何处理某个资源,我们来学习一下 HTTP 协议中缓存相关的几个关键字。

ETag

ETag 全称 Entity tag ,中文名叫实体标签。其背后主要的思想是允许浏览器在不下载完整文件的情况下了解对相关资源的修改。服务器可以计算每个文件的哈希和,然后将这个哈希和发送给客户端。下次客户端尝试访问此资源时,浏览器将在 HTTP 头中添加类似的消息: If-None-Match: W/“1d2e7–1648e509289” 。然后,服务器将对照当前文件的哈希和检查此哈希和,如果存在差异,则强制客户端下载新文件。否则,将通知客户端应该使用缓存版本。

打开 ETag 缓存策略后,我们总是到服务器检查文件的哈希和,只有在这之后,浏览器才会决定将其从缓存中取出或完全加载。当一个资源没有被修改时,不管你在请求什么,不管是一个 10 KB 还是 10 MB 的文件,只需要 80 - 100 个字节就可以进行验证。

Last-Modified

另一个缓存控制设置是 Last-Modified ,主要的想法与 ETag 非常相似,但是浏览器的行为有点不同。服务器针对每个文件都有一个最后修改日期的时间戳,在第一个文件加载之后,客户端可以询问服务器自客户端上次访问文件以来是否修改了资源。为此,浏览器会在 HTTP 响应头中发送 If-Modified-Since: Fri, 13 Jul 2018 10:49:23 GMT 。如果资源已被修改,则浏览器必须下载新文件,否则将使用缓存版本。

实际情况是,浏览器有其内部的缓存策略,可以自行决定是否从缓存中获取资源或者下载新副本。

Last-Modified 是一个弱缓存头,因为浏览器应用启发式方法来确定是否从缓存中提取项,并且启发式方法在浏览器之间有所不同。

引自 谷歌缓存最佳实践指南

因此,我们不能只依赖上次修改的内容,所以我宁愿将其从服务器设置中完全删除,以减少通信量,即使只是几个字节。

Cache-Control

Cache-Control 头被定义为 HTTP/1.1 规范的一部分,并取代了之前用于定义响应缓存策略的头信息(比如 Expires )。所有的现代浏览器都支持 Cache-Control ,因此我们用这些就够了。

max-age

这项设置使我们能告诉浏览器自文件第一次加载以来它应该在缓存中保存多长时间。浏览器将文件保存在缓存中的时间以秒为单位定义,通常显示如下: Cache-Control: max-age=31536000 。使用此策略,浏览器将完全跳过向服务器发出请求的过程,并快速打开文件。但是我们怎么能确定那个文件在这么长的时间内不会改变呢?我们不能确定。

因此,为了强制浏览器下载所需文件的新版本,我们使用了由许多前端构建工具(如 Webpack 或 Gulp )实现的技术。每个文件将在服务器上预编译,并将哈希和添加到文件名中,例如 app-72420c47cc.css 。即使对文件的微小更改也会反映在哈希和中,这保证了它将被识别为不同的。因此,在下一次部署之后,浏览器就会获得该文件的新版本。这可以应用于所有的 CSS、JS 和图片文件( max age=31536000 );在我们更改某些内容之后,浏览器将只请求一个具有新哈希和的新文件,然后将其缓存。

no-cache

上述缓存控制方案还有个棘手问题没有解决,那就是对 HTML 文件缓存控制,如果将这些设置应用于 HTML 文件,则在强制重新加载之前永远不会获得 CSS、JS 或图片文件的新链接。

我建议针对 HTML 文件使用 Cache-Control: no-cache 。使用 no-cache 并不意味着根本没有缓存,它只是告诉浏览器在从缓存中读取之前先验证服务器上的资源。这就是我们需要将其与 ETag 一起使用的原因,因此浏览器将发送一个简单的请求并加载额外的 80 个字节来验证文件的状态。

no-cache (不缓存)与 no-store(不保存)

no-cache 表示对相同的 URL 进行后续请求时,返回的响应在未经服务器检查其是否被修改之前,不能使用。因此,如果提交一个适当的验证令牌( ETage ),no-cache 会增加一个往返来验证已缓存响应,但却可以在响应未发生更改的时候,避免下载。

与之相比,no-store 就更简单了,它直接禁止浏览器和所有的中继缓存储存任何版本的返回响应——比如包含了个人隐私或者银行信息的响应。每当用户请求这个资源,都会向服务器发送一个请求,并下载到完整的响应。

public (公开)与 private (私有)

如果某个响应被标记为 public,那么该响应可以被缓存,即使它有与之相连的HTTP认证,甚至响应状态代码不是正常可缓存的。大多数时候,public 不是必需的,因为显式缓存信息(像是 max-age )就指明了该缓存是可以缓存的。

与之相对, private 响应可以由浏览器缓存,但是通常只针对单个用户,因此不允许由任何中继缓存缓进行缓存——比如说,一个带有用户个人信息的 HTML 页面可以让用户浏览器缓存,但是不能让 CDN 缓存。

最终设置

  • 使用 Gulp、Webpack 或类似工具向 CSS、JS 和图片的文件名中添加唯一的哈希数字(如 app-67ce7f3483.css )。
  • 对于 JS、CSS 和图片文件,设置 Cache-Control: public,max-age=31536000 ,无 ETag,无 Last-Modified 设置。
  • 对于 HTML 文件,使用 Cache-Control: no-cacheETag