译:深度调研 React 开发者的初始加载性

原文:https://www.developerway.com/posts/initial-load-performance
作者:Nadia
译者:ChatGPT 4 Turbo

编者注:这篇文章深入探讨了 React 应用的初始加载性能。主要内容包括:1) 核心性能指标:首字节时间 (TTFB)、首次绘制 (FP)、首次内容绘制 (FCP) 和最大内容绘制 (LCP),其中 LCP 应该控制在 2.5 秒以内;2) 性能分析工具:介绍了如何使用 Chrome DevTools 的 Performance 和 Lighthouse 面板来分析和优化性能;3) 网络条件对性能的影响:通过模拟不同的网络环境(带宽和延迟),展示了它们如何影响加载性能,特别强调了高延迟比低带宽更容易成为性能瓶颈;4) 缓存策略:详细讨论了 Cache-Control 头部的配置以及现代打包工具如何通过文件哈希来优化缓存策略。文章通过实际例子和清晰的图示,帮助开发者理解和优化网站的初始加载性能。

探索核心网络指标(Core Web Vitals)、性能开发工具、什么是初始加载性能、哪些指标可以衡量它,以及缓存控制和不同的网络条件是如何影响它的。

React 开发者的初始加载性能:深入调查

如今,随着 AI 驱动的代码生成蓬勃发展,编写 React 代码的重要性正在减小。现在,任何人和任何事都可以用 React 编写应用程序。但编写代码始终只是解决问题的一部分。我们仍然需要将我们的应用部署在某处,向用户展示它们,使它们健壮,使它们快速,并做其他一百万件事。至少目前还没有 AI 能够接管这些。

所以,让我们今天专注于使应用变得快速。为此,我们需要暂时离开 React。因为在使某物变快之前,我们首先需要知道什么是“快”,如何衡量它,以及什么可以影响这种“快速”。

剧透警告:除了研究项目之外,本文中将不涉及 React。今天我们将讨论的都是基础内容:如何使用性能工具,核心网络指标(Core Web Vitals)简介,Chrome 性能面板,什么是初始加载性能,哪些指标可以衡量它,以及缓存控制和不同的网络条件是如何影响它的。

初始加载性能指标介绍

当我打开浏览器并尝试导航到我最喜欢的网站时会发生什么?我在地址栏中输入 “http://www.my-website.com”,浏览器向服务器发送一个 GET 请求,并返回一个 HTML 页面。

image-20250101-092924.png

这个过程所花费的时间被称为“首字节时间”(TTFB):从发送请求到开始接收结果之间的时间。在收到 HTML 后,浏览器现在需要尽快将这个 HTML 转换成一个可用的网站。

它首先在屏幕上渲染所谓的“关键路径”:可以向用户显示的最小且最重要的内容。

working-on-critical-path-20250102-003450.png

关键路径中究竟应该包含什么是一个复杂的问题。理想情况下,是一切内容,这样用户立刻就能看到完整的体验。但同时也是 – 没有任何内容,因为它需要尽可能快,毕竟它是一个“关键”路径。两者同时发生是不可能的,因此需要有所妥协。

妥协看起来是这样的。浏览器假设为了构建“关键路径”,它绝对需要至少以下类型的资源:

  • 它从服务器接收到的初始 HTML – 用于构建实际的 DOM 元素,从而构建体验。
  • 重要的 CSS 文件,用于为这些初始元素_设置样式_ – 否则,如果它在没有等待它们的情况下继续进行,用户会在最开始看到一个奇怪的“未样式内容闪烁”。
  • 修改布局的关键 JavaScript 文件。

第一个(HTML)浏览器在从服务器的初始请求中获取。它开始解析它,并在此过程中提取它需要完成“关键路径”的 CSS 和 JS 文件的链接。然后它发送请求从服务器获取它们,等待它们被下载,处理它们,将所有这些结合在一起,最终在某个时刻,将“关键路径”的像素绘制在屏幕上。

由于浏览器在没有这些关键资源的情况下无法完成初始渲染,它们被称为“渲染阻塞资源”。当然,并非所有的 CSS 和 JS 资源都是渲染阻塞的。通常只有:

  • 大部分 CSS,无论是内联的还是通过 <link> 标签的。
  • <head> 标签中的 JavaScript 资源,这些资源不是 asyncdeferred

渲染“关键路径”的整个过程大致如下:

  • 浏览器开始解析初始 HTML
  • 在此过程中,它从 <head> 标签中提取 CSS 和 JS 资源的链接。
  • 然后,它启动下载过程并等待阻塞资源完成下载。
  • 等待时,如果可能,它会继续处理 HTML。
  • 收到所有关键资源后,也会对它们进行处理。
  • 最后,它完成需要做的任何事情,并绘制界面的实际像素。

这个时间点就是我们所说的首次绘制(FP)。这是用户首次有机会在屏幕上看到某些内容的时刻。这是否会发生取决于服务器发送的 HTML。如果那里有一些有意义的内容,比如文本或图片,那么这个点也将是 首次内容绘制(FCP)发生的时刻。如果 HTML 只是一个空的 div,那么 FCP 将会晚些时候发生。

second-step-20250102-033953.png

首次内容绘制(FCP) 是最重要的性能指标之一,因为它衡量了_感知的初始加载_。基本上,它是用户对您网站速度的第一印象。

直到这一刻,用户只是在盯着空白屏幕咬指甲。根据谷歌的说法,一个好的 FCP 数字是低于 1.8 秒。之后,用户将开始对您的网站能提供什么失去兴趣,并可能开始离开。

然而,FCP 并不完美。如果网站以一个旋转器或某种加载屏幕开始其加载过程,FCP 指标将代表这一点。但用户不太可能仅仅为了查看花哨的加载屏幕而导航到网站。大多数时候,他们想要访问内容。

为此,浏览器需要完成它开始的工作。它等待其余的非阻塞 JavaScript 完成,执行它,将由它引起的更改应用到屏幕上的 DOM,下载图片,并以其他方式提升用户体验。

在这个过程的某个地方,会发生 最大内容绘制(LCP)时间。与 FCP(首次内容绘制)不同,它代表页面上的主要内容区域 – 视口中可见的最大文本、图像或视频。根据 Google 的说法,这个数字理想情况下应该低于 2.5 秒。超过这个时间,用户会认为网站很慢。

third-step-20250102-051229.png

所有这些指标都是 Google 的 Web Vitals 的一部分 – 一组代表页面上用户体验的指标。LCP 是三个核心 Web Vitals之一 – 三个代表用户体验不同方面的指标。LCP 负责**加载性能**。

这些指标可以通过 Lighthouse 来测量。Lighthouse 是一个 Google 性能工具,集成在 Chrome DevTools 中,也可以通过 shell 脚本、Web 界面或 node 模块运行。你可以以 node 模块的形式使用它,在构建中运行它,以便在问题影响生产之前检测到回归。使用集成的 DevTools 版本进行本地调试和测试。使用 Web 版本来检查竞争对手的性能。

性能 DevTools 概览

以上所有内容都是对过程的非常简短和简化的解释。但这已经是很多缩写和理论,足以让人头晕目眩。对我个人而言,阅读这样的内容毫无用处。除非我能亲手实践并玩转它,否则我会立刻忘记一切。

对于这个特定的话题,我发现完全理解这些概念的最简单方法是在半真实页面上模拟不同的场景,看看它们如何改变结果。所以,在进行更多的理论学习之前(而且还有更多!),让我们确切地这样做。

设置项目

如果你愿意,你可以在自己的项目上进行以下所有模拟 – 结果应该或多或少相同。然而,为了一个更受控和简化的环境,我建议使用我为这篇文章准备的学习项目。你可以在这里访问它:https://github.com/developerway/initial-load-performance

首先安装所有依赖:

npm install

构建项目:

npm run build

并启动服务器:

npm run start

你应该在 “http://localhost:3000” 看到一个漂亮的仪表板页面。

探索必要的 DevTools

在 Chrome 中打开你想要分析的网站,并打开 Chrome DevTools。在那里找到 “Performance” 和 “Lighthouse” 面板,并将它们移动到一起。我们两个都需要。

此外,在本文中做任何其他事情之前,请确保你启用了 “Disable cache” 复选框。它应该在最顶部的 Network 面板中。

disable-cache-20250103-044948.png

这样我们就可以模拟首次访问者 – 那些之前从未访问过我们网站并且浏览器中还没有任何资源缓存的人。

探索 Lighthouse 面板

现在打开 Lighthouse 面板。你应该在那里看到一些设置和 “Analyze page load” 按钮。

lighthouse-panel-20250103-002418.png

我们在这一部分感兴趣的是 “Navigation” 模式 – 它将对页面的初始加载进行详细分析。报告将给你这样的分数:

lighthouse-scores-20250103-003350.png

本地性能是完美的,毫不奇怪 – 一切总是 “在我的机器上工作”。

也会有像这样的指标:

lighthouse-metrics-20250103-003618.png

我们在这篇文章中需要的 FCP 和 LCP 值就在顶部。

下面,你会看到一个建议列表,这些建议可以帮助你提高分数。

lighthouse-suggestions-20250103-004020.png

每个建议都可以展开,你会在那里找到更详细的信息,有时还有解释该特定主题的链接。并非所有建议都可以采取行动,但这是一个了解性能并了解可以改善性能的不同事物的绝佳工具。仅仅阅读这些报告和相关链接就可能花费数小时。

然而,Lighthouse 只提供表面级别的信息,不允许你模拟不同的场景,如慢速网络或低 CPU。它只是一个很好的入门点和一个跟踪性能变化的绝佳工具。要深入了解发生了什么,我们需要 “Performance” 面板。

探索 Performance 面板

首次加载时,Performance 面板应该看起来像这样:

performance-panel-first-load-20250103-004800.png

它显示了三个 Core Web Vitals 指标,其中一个是我们的 LCP,让你能够模拟慢速网络和 CPU,并且能够记录随时间变化的性能详情。

在面板顶部找到并勾选 “Screenshots” 复选框,然后点击 “Record and reload” 按钮,当网站自己重新加载时 – 停止录制。这将是你关于页面在初始加载期间发生了什么的详细报告。

这份报告将有几个部分。

最顶部是 “timeline overview” 部分。

performance-panel-report-20250103-015743.png

在这里,你能看到网站上正在发生一些事情,但没有更多的信息。当你悬停在它上面时 – 正在发生的事情的截图会出现,你将能够选择并放大到特定范围以更仔细地查看。

下面是一个网络部分。展开后,你会看到所有正在下载的外部资源以及它们在时间线上的确切时间。当你悬停在特定资源上时,你会看到关于下载的每个阶段花费了多少时间的详细信息。带有红色角落的资源将指示阻塞资源。

performance-panel-network-20250103-020031.png

如果你正在研究项目工作,你会看到完全相同的画面,而且这个画面与我们在上一节中经历的完全一致:

  • 开始时,有一个蓝色块 – 一个请求以获取网站的 HTML
  • 它加载完成后,稍作停顿(以解析 HTML),两个请求更多资源的请求被发送出去。
  • 其中一个(黄色的)是请求 JavaScript – 不阻塞。
  • 另一个(紫色的)是请求 CSS,这个是阻塞的。

如果你现在打开你的研究项目代码并窥视 dist 文件夹,源代码与这种行为匹配:

  • 会有一个 index.html 文件和 assets 文件夹内的 .css.js 文件
  • index.html 文件的 <head> 部分内,会有一个指向 CSS 文件的 <link> 标签。我们知道,<head> 中的 CSS 资源是渲染阻塞的,所以这是对的。
  • 同样,在 <head> 内有一个指向 asset 文件夹内 JavaScript 文件的 <script> 标签。它既不是 deferred 也不是 async,但它有 type="module"。这些是自动延迟的,所以这也是对的 – 面板中的 JavaScript 文件是非阻塞的。

附加练习
如果你正在进行一个项目,记录它的初始加载性能,并查看 Network 面板。你很可能会看到下载了更多的资源。

  • 你有多少个渲染阻塞资源?它们都是必需的吗?
  • 你知道你的项目的“入口”点在哪里,以及如何在 <head /> 部分出现阻塞资源吗?尝试使用你的 npm build 变体构建项目并搜索它们。提示:
  • 如果你有一个纯 webpack 基础的项目,查找 webpack.config.js 文件。HTML 入口点的路径应该在里面。
  • 如果你使用的是 Vite,查看 dist 文件夹 – 同学习项目一样
  • 如果你使用的是 Next.js App 路由器 – 瞥一眼 .next/server/app 文件夹

在 Network 部分,你可以找到 FramesTiming 部分。

performance-panel-frames-20250103-020206.png

这些非常酷。在 Timing 部分,你可以看到我们之前讨论过的所有指标(FP、FCP、LCP),以及一些我们还没有讨论的指标。当鼠标悬停在指标上时,你可以看到它所花费的确切时间。点击它们将更新最底部的“摘要”标签页,在那里你会找到关于这个指标的信息以及学习更多的链接。DevTools 现在都是关于教育人们的。

最后,Main 部分。这是在记录的时间线中主线程发生的事情。

performance-panel-main-20250103-020435.png

我们可以在这里看到像“解析 HTML”或“布局”以及它们所花费的时间。黄色的部分与 JavaScript 相关,它们有点无用,因为我们使用的是带有压缩 JavaScript 的生产构建。但即使在这种状态下,它也给我们一个大概的想法,比如 JavaScript 执行所花费的时间与解析 HTML 和绘制布局相比。

在同时打开并放大 NetworkMain 以占据全屏时,这对性能分析特别有用。

performance-panel-main-network-together-20250103-034155.png

从这里,我可以看到我有一个非常快的服务器和快速且小的包。网络任务中没有一个是瓶颈;它们不占用任何显著的时间,在它们之间,浏览器只是在放松并做它自己的事情。所以,如果我想加快这里的初始加载速度,我需要查看为什么解析 HTML 这么慢 – 它是图表上最长的任务。

或者,如果我们看绝对数字 – 在性能方面,我不应该在这里做任何事情。整个初始加载耗时不到 200ms,远低于 Google 推荐的阈值 🙂 但这是因为我在本地运行这个测试(所以没有实际的网络成本),在一台非常快的笔记本电脑上,并且使用一个非常基础的服务器。

是时候模拟现实生活了。

探索不同的网络条件

非常慢的服务器

首先,让我们让服务器更加现实。现在,最初的“蓝色”步骤大约需要 50ms,其中 40ms 只是等待。

1.ideal-conditions-20250103-042928.png

在现实生活中,服务器会做事情,检查权限,生成东西,再检查两次权限(因为它有很多遗留代码,那种三重检查丢失了),并且会忙碌。

导航到你的学习项目中的 backend/index.ts 文件(https://github.com/developerway/initial-load-performance)。找到被注释的 // await sleep(500),并取消注释它。这将使服务器在返回 HTML 之前延迟 500ms – 对于一个旧的和复杂的服务器来说,这似乎是合理的。

重新构建项目(npm run build),重新启动(npm run start)并重新运行性能记录。

时间线上除了最初的蓝线之外,没有任何变化 – 现在与其他内容相比,它变得异常长。

2.slow-server-20250103-045502.png

这种情况突显了全面观察和在进行任何性能优化之前识别瓶颈的重要性。LCP 值约为 650ms,其中约 560ms 被用于等待初始 HTML。React 部分大约为 50ms。即使我设法将其减半,减少到 25ms,在整体情况下,它只占 4%。而将其减半将需要_大量_的努力。一个更有效的策略可能是专注于服务器并弄清楚为什么它这么慢。

模拟不同的带宽和延迟

并不是每个人都生活在 1-吉比特连接的世界中。例如,在澳大利亚,50 兆比特/秒是一种高速互联网连接,每月将花费你大约 90 澳大利亚元。当然,这不是 3G,世界上许多人仍然在使用。但是,每当我听到欧洲人吹嘘他们的 1 吉比特/秒或 10 欧元的互联网计划时,我还是会哭泣。

无论如何。让我们模拟这种不那么出色的澳大利亚互联网,看看性能指标会发生什么变化。为此,清除性能标签中的现有记录(重新加载和记录旁边的按钮)。网络设置面板应该会出现:

3.environment-settings-20250103-053147.png

如果在你的 Chrome 版本中没有出现,相同的设置应该在“网络”标签中可用。

在“网络”下拉菜单中添加一个新的配置文件,使用以下数字:

  • 配置文件名称:“平均互联网带宽”
  • 下载:50000(50 Mbps)
  • 上传:15000(15 Mbps)
  • 延迟:40(对于一般的互联网连接来说大约是平均水平)

4.network-throttling-20250103-053038.png

现在在下拉菜单中选择该配置文件,然后再次运行性能记录。

你看到了什么?对我来说,它看起来像这样。

LCP 值几乎没有变化 – 从 640ms 稍微增加到了 700ms。最初的蓝色“服务器”部分没有任何变化,这是可以解释的:它只发送最基本的 HTML,所以下载它不应该花很长时间。

但是可下载资源与主线程之间的关系发生了巨大变化。

5.average-internet-20250103-055702.png

我现在可以清楚地看到 渲染阻塞 CSS 文件的影响了。解析 HTML 任务已经完成,但浏览器正在等待 CSS – 在下载完成之前,什么都不能被绘制。与之前的图片相比,那时资源几乎是在浏览器解析 HTML 的同时立即下载的。

之后,从技术上讲,浏览器本可以绘制一些内容 – 但实际上没有任何内容,我们只在 HTML 文件中发送了一个空的 div。所以浏览器继续等待,直到 javascript 文件被下载并可以执行。

这大约 60ms 的等待间隙正是我所看到的 LCP 增加的部分。

为了看到它是如何进展的,再进一步降低速度。创建一个新的网络配置文件,下载和上传速度分别为 10mbps/1mbps,保持 40 的延迟,并将其命名为“低互联网带宽”。

6.low-bandwidth-20250103-230711.png

然后再次运行测试。

LCP 值现在增加到了将近 500 ms。JavaScript 下载几乎需要 300 ms。而且,从相对重要性来说,解析 HTML 任务和执行 JavaScript 任务的重要性正在减小。

额外练习
如果你有自己的项目,尝试在其上运行这个测试。

  • 下载所有关键路径资源需要多长时间?
  • 下载所有 JavaScript 文件需要多长时间?
  • 这次下载在解析 HTML 任务之后会造成多大的间隙?
  • 在主线程中,解析 HTML 和执行 JavaScript 任务相对于资源下载的大小如何?
  • 它如何影响 LCP 指标?

资源条内部发生的事情也相当有趣。将鼠标悬停在黄色的 JavaScript 条上。你应该会在那里看到这样的内容:

7.javascript-bar-hover-20250103-235626.png

这里最有趣的部分是“请求发送并等待”,大约需要 40 ms。将鼠标悬停在其余的网络资源上 – 它们都会有这个。这就是我们设置为 40 的 延迟,即网络延迟。许多因素可以影响延迟数值。网络连接的类型就是其中之一。例如,平均的 3G 连接将有 10/1 Mbps 的带宽和 100 到 300 ms 之间的延迟。

为了模拟这一点,创建一个新的网络配置文件,命名为“平均 3G”,从“低互联网带宽”配置文件复制下载/上传数值,并将延迟设置为 300 ms。

再次运行分析。所有网络资源应该将“请求发送并等待”增加到大约 300 ms。这将进一步推高 LCP 数字:对我来说是 1.2 秒

现在有趣的部分来了:如果我将带宽恢复到超高速度但保持低延迟会发生什么?让我们尝试这个设置:

  • 下载:1000 Mbps
  • 上传:100 Mbps
  • 延迟:300 ms

如果你的服务器位于挪威,但客户是富有的澳大利亚人,这种情况很容易发生

这是结果:

8.high-speed-low-latency-20250104-014439.png

LCP 数值大约是 960ms。这比我们之前尝试的最慢的互联网速度还要糟糕!在这种情况下,包的大小并不重要,CSS 的大小完全无关紧要。即使你将它们都减半,LCP 指标也几乎不会有任何变化。高延迟胜过一切。

这让我想到了每个人如果还没有实施的话,应该首先实施的性能改进措施。它被称为“确保静态资源始终通过 CDN 提供”。

CDN 的重要性

CDN 基本上是任何与前端性能相关的事情的第 0 步,在开始考虑更高级的东西,如代码分割或服务器组件之前。

任何 CDN(内容交付网络)的主要目的是减少延迟并尽可能快地将内容交付给最终用户。他们为此实施了多种策略。对于本文来说,最重要的两个是“分布式服务器”和“缓存”。

CDN 提供商将在不同的地理位置拥有多个服务器。这些服务器可以存储您的静态资源的副本,并在浏览器请求它们时将它们发送给用户。CDN 基本上是您原始服务器周围的一个软层,它可以保护它免受外界影响,并将其与外界的互动降到最低。它有点像内向者的 AI Assistant,可以处理典型的对话,而无需涉及真实的人。

在上面的例子中,我们在挪威有服务器,在澳大利亚有客户端,我们得到了这样的图景:

1.norway-australia-20250104-030110.png

有了 CDN 的介入,情况将会改变。CDN 将在用户较近的地方拥有一个服务器,比方说也在澳大利亚的某个地方。在某个时刻,CDN 将从原始服务器接收静态资源的副本。完成这一操作后,来自澳大利亚或其附近任何地方的用户将获得这些副本,而不是来自挪威服务器的原始资源。

它实现了两件重要的事情。首先,由于用户不再需要直接访问原始服务器,因此减轻了原始服务器的负载。其次,用户现在可以更快地获取这些资源,因为他们不再需要跨越海洋去下载几个 JavaScript 文件了。

2.norway-cdn-australia-20250104-031323.png

而且,在我们上面的模拟中,LCP 值从 960ms 下降到了 640ms 🎉。

重复访问性能

直到现在,我们只讨论了首次访问性能——那些从未访问过你的网站的人的性能。但希望,网站非常好,以至于大多数首次访问者变成了常客。或者至少他们在第一次加载后不会离开,浏览几个页面,甚至买些东西。在这种情况下,我们通常期望浏览器缓存静态资源,如 CSS 和 JS——即,本地保存它们的副本,而不是总是下载它们。

让我们看看在这种情况下,性能图表和数字如何变化。

再次打开研究项目。在开发者工具中,将网络设置为我们之前创建的“平均 3G”——具有高延迟和低带宽,这样我们就可以立即看到差异。并确保“禁用网络缓存”复选框未选中。

1.initial-set-up-20250112-031042.png

首先,刷新浏览器以确保我们排除了首次访问者的情况。然后刷新并测量性能。

如果你使用的是研究项目,最终结果可能会稍有惊讶,因为它看起来像这样:

2.repeated-user-load-performance-20250112-032213.png

CSS 和 JavaScript 文件在网络标签中仍然非常突出,我在“请求发送和等待”中看到了大约 300ms——我们在“平均 3G”配置文件中的延迟设置。结果,LCP 并不像它可能那样低,当浏览器只是等待阻塞的 CSS 时,我有一个 300ms 的间隙。

发生了什么?浏览器不是应该缓存这些东西吗?

控制浏览器缓存与 Cache-Control 头部

我们现在需要使用网络面板来理解发生了什么。打开它,找到 CSS 文件。它应该看起来像这样:

3.css-file-304-reponse-20250113-003114.png

这里最有趣的是“状态”列和“大小”。在“大小”中,它绝对不是整个 CSS 文件的大小。它太小了。而在“状态”中,它不是我们正常的 200 “一切正常”的状态,而是有所不同 – 304 状态。

这里有两个问题 – 为什么是 304 而不是 200,以及为什么会发送请求?缓存为什么没有工作?

首先,304 响应。这是一个配置良好的服务器为条件请求发送的响应 – 响应基于各种规则变化。这样的请求经常被用来控制浏览器缓存。

例如,当服务器收到一个 CSS 文件的请求时,它可以检查文件最后修改的时间。如果这个日期与浏览器端缓存的文件相同,它返回一个空体的 304(这就是为什么它只有 223 B)。这向浏览器表明,它可以安全地重新使用它已经拥有的文件。没有必要浪费带宽并再次下载它。

这就是为什么我们在性能图片中看到大量的“请求发送并等待”数字 – 浏览器请求服务器确认 CSS 文件是否仍然是最新的。这也是为什么“内容下载”是 0.33ms – 服务器以“304 未修改”响应,浏览器只是重新使用了它之前下载的文件。

现在,对于第二个问题 – 为什么会发送这个请求?

这种行为由服务器设置的Cache-Control头部控制。在网络面板中点击 CSS 文件,查看请求/响应的详细信息。在“响应头部”块的“头部”标签中找到“Cache-Control”的值:

在这个头部中,可以有多种不同组合的指令,通过逗号分隔。在我们的案例中,有两个:

  • max-age 后面跟一个数字 – 它控制这个特定响应将被存储多长时间(以秒为单位)
  • must-revalidate – 它指示浏览器如果响应变得陈旧,总是向服务器发送请求以获取新版本。如果响应在缓存中的时间超过了 max-age 的值,响应就会变得陈旧。

所以,基本上,这个头部告诉浏览器的是:

  • 把这个响应存储在你的缓存中是可以的,但在一段时间后要与我再次确认以确保。
  • 顺便说一下,你可以保留该缓存的时间确切地是 秒。祝你好运。

结果是,浏览器 总是 与服务器核对,而不是立即使用缓存。

不过,我们可以轻松改变这一点 – 我们需要做的就是将 max-age 的数字改为 0 到 31536000(一年,允许的最大秒数)之间的某个值。为此,在你的学习项目中,转到 backend/index.ts 文件,找到 max-age=0 设置的地方,并将其更改为 31536000(一年)。刷新页面几次,你应该在网络标签中看到 CSS 文件的情况如下:

5.memory-cache-in-network-panel-20250113-044543.png

注意现在 Status 列是灰色的,对于 Size,我们看到的是“(memory cache)”。CSS 文件现在是从浏览器的缓存中提供的,并且在接下来的一年中都会是这样。刷新页面几次,看看它是否改变。

现在,让我们来到搞乱缓存头部的整个重点:让我们再次测量页面的性能。不要忘记设置“平均 3G”配置文件设置,并保持“禁用缓存”设置未选中。

结果应该是这样的:

6.cache-performance-20250113-045302.png

尽管延迟很高,但“请求发送并等待”的部分几乎降至零,“解析 HTML” 和 JavaScript 评估之间的间隙几乎消失了,我们又回到了大约 650ms 的 LCP 值。

Cache-Control 和现代打包工具

上述信息是否意味着缓存是我们的性能银弹,我们应该尽可能积极地缓存一切?绝对不是!除了其他一切,为“不懂技术的客户”和“需要通过电话解释如何清除浏览器缓存”的组合创造机会,将会让最有经验的开发者感到恐慌。

有数以百万计的方式来优化缓存,Cache-Control 头中的指令与其他可能会或可能不会影响缓存存活时间的头的组合,这也可能会或可能不会取决于服务器的实现。可能仅就这个话题就可以写几本书。如果你想成为缓存大师,从 https://web.dev/MDN 资源上的文章开始,然后跟随面包屑。

不幸的是,没有人能告诉你,“这是适用于一切的五个最佳缓存策略。”最好的答案可能是:“如果你有这个用例,结合这个、这个和这个,那么这个缓存设置组合是一个不错的选择,但要注意这些小问题”。这一切都归结于了解你的资源、你的构建系统、资源变更的频率、缓存它们的安全性以及如果做错了会有什么后果。

然而,有一个例外。以一种有一个明确的“最佳实践”的方式例外:使用现代工具构建的网站的 JavaScript 和 CSS 文件。现代打包工具,如 Vite、Rollup、Webpack 等,可以创建“不可变”的 JS 和 CSS 文件。当然,它们并非真正的“不可变”。但这些工具为文件生成了一个依赖于文件内容的哈希字符串的名称。如果文件内容改变,那么哈希改变,文件的名称也改变。结果是,当网站部署时,无论缓存设置如何,浏览器都会重新获取文件的全新副本。缓存被“破坏”,就像之前我们手动重命名 CSS 文件时一样。

例如,看一下学习项目中的 dist/assets 文件夹。js 和 CSS 文件都有 index-[hash] 的文件名。记住这些名字,然后运行几次 npm run build。由于这些文件的内容没有改变,所以名字保持完全相同。

现在去 src/App.tsx 文件中添加类似 console.log('bla') 的东西。再次运行 npm run build,并检查生成的文件。你应该会看到 CSS 文件名保持和之前完全一样,但是 JS 文件名改变了。当这个网站部署后,下次重复用户访问它时,浏览器将请求一个之前从未出现在其缓存中的完全不同的 JS 文件。缓存被破坏了。

如果你的构建系统是这样配置的 – 你很幸运。你可以安全地配置你的服务器,为生成的资源设置最大的 max-age 头。如果你以类似方式为所有图片版本化 – 更好,你也可以将图片包含在列表中。

根据网站及其用户和他们的行为,这可能会为你免费提供相当不错的初始加载性能提升。

我真的需要为我的简单用例了解所有这些吗?

到这个时候,你可能会想,“你疯了。我用 Next.js 在周末建了一个简单的网站,并在 2 分钟内部署到了 Vercel/Netlify/HottestNewProvider。当然,这些现代工具为我处理了所有这些问题吧?”。公平地说,我也是这么想的。但后来我实际检查了一下,哇,我惊讶了 😅

我的两个项目对于 CSS 和 JS 文件有 max-age=0must-revalidate。原来这是我的 CDN 提供商的默认设置 🤷🏻‍♀️。当然,他们对这个默认设置有原因。幸运的是,这很容易覆盖,所以没什么大不了的。但还是。这些天不能信任任何人或任何东西 😅。

那么你的托管/CDN 提供商呢?你对他们的缓存头配置有多确定?