缓存头:面向前端开发人员的实用指南
由MichaelZalecki✏️撰写
开发人员和操作人员可以使用多个标头来操纵缓存行为。
旧规范与新规范混在一起:有许多要配置的设置,您可以发现多个用户报告行为不一致。
在本文中,我将重点介绍不同的标头如何影响浏览器缓存以及它们与代理服务器的关系。
您将找到Nginx的配置示例以及运行Express的Node.js的代码。最后,我们将研究在React中创建的流行服务如何为其Web应用程序提供服务。
对于单页应用程序,我有兴趣无限期地缓存JavaScript,CSS,字体和图像文件,并希望避免缓存HTML文件和服务工作者(如果有)。
这种策略是可行的,因为我的资产文件在文件名中具有唯一的标识符。
您可以在WebPack中实现相同的配置以包含一个 [hash]
,或者-甚至更好- [chunkhash]
,在资产的文件名中。此技术称为长期缓存。
但是,当您阻止重新下载时,如何对网站进行更新?保持网站更新能力的原因就是永远不要缓存HTML文件如此重要。
每次您访问我的网站时,浏览器都会从服务器获取HTML文件的新副本,并且只有在存在新的脚本srcs或link hrefs时,浏览器才会从服务器下载新资产。
缓存控制
Cache-Control: no-store
当告知请求时,浏览器不应存储有关该请求的任何内容 no-store
。您可以将其用于HTML和Service Worker脚本。
Cache-Control: public, no-cache
or
Cache-Control: private, max-age=0, must-revalidate
这两个是等效的,尽管没有缓存名称,但允许提供缓存的响应,但浏览器必须验证缓存是否新鲜。
如果您正确设置了ETag或Last-Modified标头,以使浏览器可以验证是否已缓存了最新版本,则您和您的用户将节省带宽。您可以将其用于HTML和服务工作者脚本。
Cache-Control: private, no-cache
or
Cache-Control: private, max-age=0, must-revalidate
以此类推,这两个也是等效的。公共和私有之间的区别在于,共享缓存(例如CDN)可以缓存公共响应,但不能缓存私有响应。
本地缓存(例如浏览器)仍可以缓存私有响应。在服务器上呈现HTML时,您将使用private,并且呈现的HTML包含特定于用户的信息或敏感信息。
用框架的术语来说,您不需要为典型的Gatsby博客设置私有属性,但是对于需要授权访问的页面,应该将其与Next.js一起考虑。
Cache-Control: public, max-age=31536000, immutable
在此示例中,浏览器将根据max-age指令(60_60_24 * 365)缓存响应一年。
不可变指令告诉浏览器此响应(文件)的内容不会改变,并且浏览器不应通过发送If-None-Match(ETag验证)或If-Modified-Since(最后修改)来验证其缓存。验证)。
用途用于您的静态资产,以支持长期缓存策略。
语用和过期
Pragma: no-cache
Expires: <http-date>
Pragma是HTTP / 1.0规范中定义为请求标头的旧标头。
后来,HTTP / 1.1规范指出 Pragma: no-cache
回应应作为 Cache-Control: no-cache
,但由于它仍然是请求标头,因此它不是可靠的替代方法。
我也一直在用 Pragma: no-cache
作为OWASP安全建议。
包括 Pragma: no-cache
标头是一种预防措施,可以保护不支持较新的缓存控制机制并且可以缓存您不打算缓存的旧服务器。
有人会说,除非必须支持Internet Explorer 5或Netscape,否则就不需要Prama或Expires。它归结为对遗留软件的支持。
代理普遍了解Expires标头,这使其略有优势。
对于HTML文件,我禁用Expires标头或将其设置为过去的日期。对于静态资产,我通过Nginx expires指令与Cache-Control的max-age一起进行管理。
电子标签
ETag: W/"5e15153d-120f"
or
ETag: "5e15153d-120f"
ETag是缓存验证的几种方法之一。 ETag必须唯一地标识资源,并且大多数情况下,Web服务器会根据资源内容生成指纹。
资源更改时,它将具有不同的ETag值。
ETag有两种类型。 ETags相等性弱表示资源在语义上是等效的。强大的ETags验证表明资源在字节之间是相同的。
您可以通过为弱ETag设置的“ W /”前缀来区分两者。
弱ETag不适用于字节范围请求,但很容易即时生成。
实际上,您不会自己设置ETag并让您的Web服务器处理它们。
curl -I <http-address>
curl -I -H "Accept-Encoding: gzip" <http-address>
您可能会看到,当您从Nginx请求静态文件时,它会设置一个强大的ETag。启用gzip压缩后,如果您不上传压缩文件,则即时压缩会导致ETag变弱。
通过发送带有已缓存资源的ETag的“ If-None-Match”请求标头,浏览器将期望200 OK响应以及一个新资源,或者返回一个空的304 Not Modified响应,这表明您应该使用缓存的资源而不是下载新的。
相同的优化可以应用于API GET响应,并且不仅限于静态文件。
如果您的应用程序接收到较大的JSON有效负载,则可以将后端配置为根据有效负载的内容计算和设置ETag(例如,使用md5)。
在将其发送给客户端之前,将其与“ If-None-Match”请求标头进行比较。
如果匹配,则发送304 Not Modified而不发送有效负载,以节省带宽并提高Web应用程序性能。
上一次更改
Last-Modified: Tue, 07 Jan 2020 23:33:17 GMT
Last-Modified响应标头是另一种缓存控制机制,并使用上次修改日期。 Last-Modified标头是用于更精确的ETag的后备机制。
通过发送带有缓存资源的最后修改日期的“ If-Modified-Since”请求标头,浏览器期望使用更新资源的200 OK响应或空304 Not Modified响应,这表明缓存资源应为而不是下载新的。
调试
设置标头然后测试配置时,请确保您就网络而言离服务器很近。我的意思是,如果您对服务器进行了Docker化,则运行该容器并在本地对其进行测试。
如果配置了VM,则SSH到该VM并在其中测试标头。如果您有Kubernetes集群,请启动一个pod并从集群内调用服务。
在生产设置中,您将使用负载Balancer,代理和CDN。在上述每个步骤中,您都可以修改标头,因此,首先知道服务器已发送正确的标头,调试起来就容易得多。
例如,如果启用了电子邮件地址混淆或自动HTTPS重写,则Cloudflare会删除ETag标头,这是意外行为的一个示例。
祝您好运,尝试通过更改服务器配置进行调试在Cloudflare的辩护中,此行为已得到很好的记录,并且非常合理,因此您必须了解自己的工具。
Cache-Control: max-age=31536000
Cache-Control: public, immutable
在本文的前面,我在代码段的标头之间放置了“或”,以表明这是两个不同的示例。有时您可能会在HTTP响应中注意到多个同一个标头。
这意味着两个标头都适用。一些代理服务器可以沿途合并标头。上面的例子等于:
Cache-Control: max-age=31536000, public, immutable
使用 curl
将为您提供最一致的结果,并在多个环境中轻松运行。
如果决定无论如何都使用Web浏览器,请确保在调试缓存问题时查看服务工作者。服务人员调试是另一篇文章的复杂主题。
要解决缓存问题,请确保在“ DevTools应用程序”选项卡中启用绕过服务工作器。
Nginx配置
既然您了解了不同类型的缓存头的作用,那么现在该专注于将知识付诸实践。
以下Nginx配置将服务于为支持长期缓存而构建的单页应用程序。
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
首先,我为最有利于单页应用程序的内容类型启用了gzip压缩。有关每个可用gzip设置的更多详细信息,请访问nginx gzip模块文档。
location ~* (.html|/sw.js)$ {
expires -1y;
add_header Pragma "no-cache";
add_header Cache-Control "public";
}
我想将所有HTML文件与 /sw.js
,这是服务工作者脚本。
都不应该缓存。 Nginx expires
指令设置为负值设置超过 Expires
标头,并添加一个额外的 Cache-Control: no-cache
标头。
location ~* .(js|css|png|jpg|jpeg|gif|ico|json)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
我想最大化所有静态资产的缓存,这些静态资产包括JavaScript文件,CSS文件,图像和静态JSON文件。如果托管字体文件,则也可以添加它们。
location / {
try_files $uri $uri/ =404;
}
if ($host ~* ^www.(.*)) {
set $host_without_www $1;
rewrite ^(.*) https://$host_without_www$1 permanent;
}
这两个与缓存无关,但是它们是Nginx配置的重要组成部分。
由于现代的单页应用程序支持漂亮URL的路由,所以我的静态服务器不知道它们。我需要默认 index.html
每条与静态文件都不匹配的路由。
我也对从网址重定向到 www.
到没有网址 www
。如果托管服务提供商已经为您服务的应用程序,则您可能不需要最后一个。
快速配置
有时我们无法使用像Nginx这样的反向代理服务器来提供静态文件。
无服务器设置/服务提供商可能会限制您使用一种流行的编程语言,而性能并不是您的主要考虑。
在这种情况下,您可能需要使用Express等服务器来提供静态文件。
import express, { Response } from "express";
import compression from "compression";
import path from "path";
const PORT = process.env.PORT || 3000;
const BUILD_P新高 = "public";
const app = express();
function setNoCache(res: Response) {
const date = new Date();
date.setFullYear(date.getFullYear() - 1);
res.setHeader("Expires", date.toUTCString());
res.setHeader("Pragma", "no-cache");
res.setHeader("Cache-Control", "public, no-cache");
}
function setLongTermCache(res: Response) {
const date = new Date();
date.setFullYear(date.getFullYear() + 1);
res.setHeader("Expires", date.toUTCString());
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
}
app.use(compression());
app.use(
express.static(BUILD_P新高, {
extensions: ["html"],
setHeaders(res, path) {
if (path.match(/(.html|/sw.js)$/)) {
setNoCache(res);
return;
}
if (path.match(/.(js|css|png|jpg|jpeg|gif|ico|json)$/)) {
setLongTermCache(res);
}
},
}),
);
app.get("*", (req, res) => {
setNoCache(res);
res.sendFile(path.resolve(BUILD_P新高, "index.html"));
});
app.listen(PORT, () => {
console.log(`Server is running http://localhost:${PORT}`);
});
这个脚本模仿了我们的Nginx配置。使用压缩中间件启用gzip。
Express静态中间件集 ETag
和 Last-Modified
标头。我们必须处理发送 index.html
如果请求与任何已知的静态文件都不匹配,我们将自行决定。
例子
最后,我想探讨流行的服务如何利用缓存头。
我分别检查了HTML和CSS或JavaScript文件的标题。我还查看了服务器标头(如果有的话),因为它可以使我们对底层基础结构有令人兴奋的了解。
推特
Twitter极力尝试使其HTML文件不会出现在您的浏览器缓存中。看来Twitter正在使用Express为我们服务
无论出于何种原因,Twitter都会使用 Expiry
标头和 Expires
标头丢失。
我查了一下,但没有发现任何有趣的东西。
可能是拼写错误吗?如果您知道,请发表评论。
cache-control: no-cache, no-store, must-revalidate, pre-check=0, post-check=0
expiry: Tue, 31 Mar 1981 05:00:00 GMT
last-modified: Wed, 08 Jan 2020 22:16:19 GMT (current date)
pragma: no-cache
server: tsa_o
x-powered-by: Express
Twitter没有CSS文件,可能正在使用CSS-in-JS解决方案。看起来在Amazon ECS上运行的容器化应用程序正在提供静态文件。
etag: "fXSAIt9bnXh6KGXnV0ABwQ=="
expires: Thu, 07 Jan 2021 22:19:54 GMT
last-modified: Sat, 07 Dec 2019 22:27:21 GMT
server: ECS (via/F339)
Instagram的
Instagram也不希望您的浏览器也缓存HTML,而是使用设置为2000年初的有效Expires标头;任何比当前日期早的日期都是好的。
last-modified: Wed, 08 Jan 2020 21:45:45 GMT
cache-control: private, no-cache, no-store, must-revalidate
pragma: no-cache
expires: Sat, 01 Jan 2000 00:00:00 GMT
Instagram提供的CSS和JavaScript文件均支持长期缓存,并具有ETag。
etag: "3d0c27ff077a"
cache-control: public,max-age=31536000,immutable
纽约时报
《纽约时报》也使用React,并将其文章作为服务器端呈现的页面。上次修改日期似乎是真实的日期,不会随每个请求更改。
cache-control: no-cache
last-modified: Wed, 08 Jan 2020 21:54:09 GMT
server: nginx
同时提供“ Etag”和“最后修改日期”的内容也可以长时间缓存“纽约时报”的资产。
cache-control: public,max-age=31536000
etag: "42db6c8821fec0e2b3837b2ea2ece8fe"
expires: Wed, 24 Jun 2020 23:27:22 GMT
last-modified: Tue, 25 Jun 2019 22:51:52 GMT
server: UploadServer
结论
我创建此文件的部分目的是为了组织我的知识,但我也打算将其用作配置当前和将来项目的备忘单。我希望您喜欢阅读并且也觉得它有用
如果您有任何疑问或想提出改进建议,请在下面发表评论,我们很乐意回答
插件:LogRocket,用于Web应用程序的DVR
LogRocket是一个前端日志记录工具,可让您像在您自己的浏览器中一样重播问题。 LogRocket无需猜测错误发生的原因或询问用户屏幕截图和日志暴跌,而是让您重播会话以快速了解出了什么问题。无论框架如何,它都能与任何应用完美配合,并具有用于记录来自Redux,Vuex和@ ngrx / store的其他上下文的插件。
除了记录Redux动作和状态外,LogRocket还记录控制台日志,JavaScript错误,堆栈跟踪,带有标头+正文,浏览器元数据和自定义日志的网络请求/响应。它还使用DOM来记录页面上的HTML和CSS,甚至可以为最复杂的单页面应用程序重新创建像素完美的视频。
免费试用。
高速缓存后的标题:面向前端开发人员的实用指南首先出现在LogRocket博客上。