深入探讨网页首屏加载性能及优化策略
在 AI 驱动的代码生成蓬勃发展的今天,编写 React 代码的重要性正在下降。现在任何人和任何东西都可以用 React 编写应用程序。但是编写代码一直只是难题的一部分。我们仍然需要将我们的应用程序部署到某个地方,向用户展示它们,使它们健壮,使它们快速,并做一百万件其他事情。没有 AI 可以接管这些。至少现在还不行。
因此,让我们专注于今天如何使应用程序快速运行。为此,我们需要暂时离开 React。因为在使某些东西变快之前,我们首先需要知道“快”是什么,如何衡量它,以及什么可以影响这种“快”。
剧透警告:除了学习项目之外,本文不会出现 React。今天都是关于基础知识的:如何使用性能工具,Core Web Vitals 简介,Chrome 性能面板,初始加载性能是什么,哪些指标可以衡量它,以及缓存控制和不同的网络条件如何影响它。
当我打开浏览器并尝试导航到我喜欢的网站时会发生什么?我在地址栏中输入“https://www.php.cn/link/63ea3fef646010a7255aec506626ea32 GET 请求,并接收 HTML 页面作为返回。
执行此操作所需的时间称为“Time To First Byte”(TTFB):从发送请求到结果开始到达之间的时间。接收 HTML 后,浏览器现在必须尽快将此 HTML 转换为可用的网站。
它首先在屏幕上渲染所谓的“关键路径”:可以向用户显示的最小和最重要的内容。
关键路径中究竟应该包含什么是一个复杂的问题。理想情况下,一切都是为了让用户立即看到完整的体验。但是同样 - 什么也没有,因为它需要尽可能快,因为它是一条“关键”路径。两者同时是不可能的,所以需要妥协。
妥协是这样的。浏览器假设要构建“关键路径”,它绝对至少需要以下类型的资源:
浏览器在服务器的初始请求中获取第一个(HTML)。它开始解析它,并在这样做的过程中提取它需要完成“关键路径”的 CSS 和 JS 文件的链接。然后,它发送请求以从服务器获取它们,等待它们下载完毕,处理它们,将所有这些组合在一起,并在某个时刻结束时,在屏幕上绘制“关键路径”像素。
由于浏览器在没有这些关键资源的情况下无法完成初始渲染,因此它们被称为“渲染阻塞资源”。当然,并非所有 CSS 和 JS 资源都是渲染阻塞的。通常只有:
渲染“关键路径”的整个过程大致如下所示:
这个时间点就是我们所说的首次绘制 (FP)。这是用户第一次有机会在屏幕上看到某些东西。是否会发生取决于服务器发送的 HTML。如果那里有一些有意义的东西,例如文本或图像,那么这一点也将是首次内容绘制 (FCP) 发生的时间。如果 HTML 只是一个空 div,那么 FCP 将稍后发生。
首次内容绘制 (FCP) 是最重要的性能指标之一,因为它衡量的是感知到的初始加载。基本上,这是用户对您的网站速度的初步印象。
直到这一刻,用户只是在盯着空白屏幕咬指甲。根据 Google 的说法,良好的 FCP 数字是低于 1.8 秒。之后,用户将开始对您的网站可以提供的内容失去兴趣,并可能开始离开。
但是,FCP 并不完美。如果网站以旋转器或某些加载屏幕开始加载,则 FCP 指标将表示该内容。但用户不太可能只是为了查看花哨的加载屏幕而导航到该网站。大多数时候,他们想要访问内容。
为此,浏览器需要完成它开始的工作。它等待其余的非阻塞 JavaScript,执行它,将源自它的更改应用于屏幕上的 DOM,下载图像,并以其他方式完善用户体验。
在此过程中的某个时间点,就会发生最大内容绘制 (LCP) 时间。它不是像 FCP 那样非常第一个元素,而是页面上的主要内容区域 - 视口中可见的最大文本、图像或视频。根据 Google 的说法,这个数字理想情况下应该低于 2.5 秒。超过这个数字,用户会认为网站速度慢。
所有这些指标都是 Google 的 Web Vitals 的一部分 - 一组代表页面上用户体验的指标。LCP 是三个核心 Web Vitals之一 - 三个指标代表用户体验的不同部分。LCP 负责加载性能。
这些指标可以通过 Lighthouse 来衡量。Lighthouse 是 Google 的性能工具,它集成到 Chrome DevTools 中,也可以通过 shell 脚本、Web 界面或节点模块运行。您可以将其作为节点模块使用,以便在构建中运行它并在生产环境中出现回归之前检测到它们。使用集成的 DevTools 版本进行本地调试和测试。以及 Web 版本来检查竞争对手的性能。
以上是对该过程的非常简短和简化的解释。但这已经有很多缩写和理论让人的头脑混乱。对我个人而言,阅读这样的内容是没有用的。除非我能看到它在行动中,并能亲自动手操作,否则我会立即忘记所有内容。
对于这个特定主题,我发现完全理解这些概念的最简单方法是在半真实的页面上模拟不同的场景,并查看它们如何改变结果。所以在进行更多理论(还有很多!)之前,让我们这样做。
如果您愿意,您可以在您自己的项目上进行以下所有模拟 - 结果应该或多或少相同。但是,为了更受控制和简化的环境,我建议您使用我为本文准备的学习项目。您可以在这里访问它:https://www.php.cn/link/def14e8541708294d7558fdf2126ef27
首先安装所有依赖项:
<code>npm install</code>
构建项目:
<code>npm run build</code>
启动服务器:
<code>npm run start</code>
您应该在“https://www.php.cn/link/66e8d052ec2230c66bd11ee6b5a0e3c8。
在 Chrome 中打开要分析的网站并打开 Chrome DevTools。找到那里的“性能”和“Lighthouse”面板并将它们放在一起。我们需要两者。
此外,在本文中执行任何其他操作之前,请确保已启用“禁用缓存”复选框。它应该位于最顶部的“网络”面板中。
这样我们就可以模拟首次访问者 - 从未访问过我们网站的人,并且浏览器还没有缓存任何资源。
现在打开 Lighthouse 面板。您应该在那里看到一些设置和“分析页面加载”按钮。
对于本节,我们感兴趣的是“导航”模式 - 它将对页面的初始加载进行详细分析。该报告将为您提供如下分数:
本地性能完美无缺,这并不奇怪 - 一切都“在我的机器上运行”。
还会有如下指标:
我们本文需要的 FCP 和 LCP 值就在顶部。
下面,您将看到一个可以帮助您提高分数的建议列表。
每个建议都可以展开,您将在那里找到更详细的信息,有时还会找到解释该特定主题的链接。并非所有这些都可以采取行动,但它是一个开始学习性能并了解可以改进它的不同事物的绝佳工具。仅仅阅读这些报告和相关链接就可以花费数小时。
但是,Lighthouse 只提供表面信息,不允许您模拟慢速网络或低 CPU 等不同场景。它只是一个很好的切入点和一个跟踪性能随时间变化的绝佳工具。要更深入地了解正在发生的事情,我们需要“性能”面板。
首次加载时,“性能”面板应如下所示:
它显示了三个核心 Web Vitals 指标,其中一个是我们的 LCP,使您可以模拟慢速网络和 CPU,以及随着时间推移记录性能详细信息的能力。
在面板顶部找到并选中“屏幕截图”复选框,然后单击“记录并重新加载”按钮,当网站重新加载自身时 - 停止记录。这将是您对页面在初始加载期间发生情况的详细报告。
此报告将包含几个部分。
最顶部是常规的“时间轴概述”部分。
您将在这里看到网站上正在发生某些事情,但没有更多内容。当您将鼠标悬停在其上时 - 将显示正在发生的事情的屏幕截图,并且您将能够选择并放大到特定范围以仔细查看。
在下面是网络部分。展开后,您将看到所有正在下载的外部资源以及它们在时间轴上的确切时间。当将鼠标悬停在特定资源上时,您将看到有关在下载的哪个阶段花费多少时间的详细信息。带有红色角的资源将指示阻塞资源。
如果您正在使用学习项目,您将看到完全相同的图片,并且此图片与我们在上一节中逐字逐句进行的内容相匹配:
如果您现在打开您的学习项目代码并查看 dist 文件夹,源代码将与这种行为相匹配:
如果您正在处理某个项目,请记录其初始加载性能并查看“网络”面板。您可能会看到更多正在下载的资源。
在“网络”部分下,您可以找到“帧”和“计时”部分。
这些非常酷。在“计时”部分,您可以看到我们之前讨论过的所有指标(FP、FCP、LCP),以及我们尚未讨论的一些指标。当将鼠标悬停在指标上时,您可以看到它花费的确切时间。单击它们将更新最底部的“摘要”选项卡,您将在其中找到有关此指标的信息和了解更多信息的链接。DevTools 现在都是关于教育人们的。
最后是主部分。这是在记录的时间轴期间在主线程中发生的事情。
我们在这里可以看到诸如“解析 HTML”或“布局”之类的内容以及它花费了多长时间。黄色部分与 JavaScript 相关,它们有点没用,因为我们使用的是带有压缩 JavaScript 的生产版本。但即使在这种状态下,它也能让我们大致了解 JavaScript 执行与 HTML 解析和绘制布局相比需要多长时间,例如。
当网络和主都打开并放大以占据整个屏幕时,它对于性能分析尤其有用。
从这里,我可以看出我的服务器速度非常快,捆绑包也很快很小。没有一个网络任务是瓶颈;它们不需要任何大量时间,它们之间,浏览器只是在闲逛并做它自己的事情。因此,如果我想在这里加快初始加载速度,我需要研究为什么“解析 HTML”这么慢 - 它是图表上最长的任务。
或者,如果我们查看绝对数字 - 我不应该在这里做任何事情,从性能方面来说。整个初始加载时间少于 200 毫秒,远低于 Google 建议的阈值?但这正在发生,因为我是在本地运行此测试(因此没有实际的网络成本),在一台非常快的笔记本电脑上,并且使用非常基本的服务器。
是时候模拟现实生活了。
首先,让我们使服务器更逼真。现在,第一个“蓝色”步骤大约需要 50 毫秒,其中 40 毫秒只是在等待。
在现实生活中,服务器将执行某些操作,检查权限,生成某些内容,再次检查权限(因为它有很多遗留代码,并且三遍检查丢失了),否则将很忙。
导航到学习项目中的 backend/index.ts 文件 (https://www.php.cn/link/def14e8541708294d7558fdf2126ef27)。找到注释掉的 // await sleep(500),并取消注释它。这将使服务器在返回 HTML 之前延迟 500 毫秒 - 这对于旧的复杂服务器来说似乎足够合理。
重新构建项目 (npm run build),重新启动它 (npm run start) 并重新运行性能记录。
除了初始蓝线之外,时间轴上没有任何变化 - 与其余内容相比,它现在非常长。
这种情况突出了在进行任何性能优化之前查看全局并识别瓶颈的重要性。LCP 值约为 650 毫秒,其中约 560 毫秒用于等待初始 HTML。它的 React 部分约为 50 毫秒。即使我设法将其减半并将其减少到 25 毫秒,在整体情况中,它也只有 4%。而将其减半将需要大量的努力。更有效的策略可能是专注于服务器并找出它为什么这么慢。
并非每个人都生活在 1 千兆位连接的世界中。例如,在澳大利亚,50 兆位/秒是高速互联网连接之一,每月将花费您约 90 澳元。当然,它不是 3G,全世界很多人都被困住了。但仍然,每次我听到欧洲人吹嘘他们的 1 千兆位/秒或 10 欧元的互联网计划时,我都会哭泣。
无论如何。让我们模拟这个不太好的澳大利亚互联网,看看性能指标会发生什么。为此,清除性能选项卡中的现有记录(重新加载和记录按钮附近的按钮)。网络设置面板应该显示出来:
如果它没有出现在您的 Chrome 版本中,则相同的设置应该在“网络”选项卡中可用。
在“网络”下拉菜单中添加一个新的配置文件,使用以下数字:
现在在下拉菜单中选择该配置文件并再次运行性能记录。
你看到了什么?对我来说,它看起来像这样。
LCP 值几乎没有变化 - 从 640 毫秒略微增加到 700 毫秒。初始蓝色“服务器”部分没有任何变化,这是可以解释的:它只发送最基本的 HTML,因此下载它不应该花费很长时间。
但是可下载资源和主线程之间的关系发生了巨大变化。
我现在可以清楚地看到渲染阻塞 CSS文件的影响。“解析 HTML”任务已经完成,但浏览器正在闲置并等待 CSS - 在下载之前无法绘制任何内容。将其与之前的图片进行比较,在之前的图片中,资源几乎是即时下载的,而浏览器正在解析 HTML。
之后,从技术上讲,浏览器本可以绘制某些内容 - 但没有任何内容,我们只在 HTML 文件中发送一个空 div。因此,浏览器继续等待,直到下载并执行 javascript 文件。
这个大约 60 毫秒的等待差距正是我看到的 LCP 的增加。
进一步降低速度以查看它的进展情况。创建一个新的网络配置文件,将其命名为“低互联网带宽”,从“低互联网带宽”配置文件复制下载/上传数字,并将延迟设置为 40 毫秒。
并再次运行测试。
LCP 值现在已增加到近 500 毫秒。JavaScript 下载大约需要 300 毫秒。相对而言,“解析 HTML”任务和 JavaScript 执行任务的重要性正在减小。
如果您有自己的项目,请尝试在其上运行此测试。
资源栏内部发生的事情也很有趣。将鼠标悬停在黄色 JavaScript 条上。您应该在那里看到类似这样的内容:
这里最有趣的部分是“发送请求并等待”,大约需要 40 毫秒。将鼠标悬停在其余的网络资源上 - 所有这些都将拥有它。那是我们的延迟,我们设置为 40 的网络延迟。许多事情都会影响延迟数字。网络连接的类型就是其中之一。例如,平均 3G 连接的带宽为 10/1 Mbps,延迟在 100 到 300 毫秒之间。
要模拟这一点,请创建一个新的网络配置文件,将其命名为“平均 3G”,从“低互联网带宽”配置文件复制下载/上传数字,并将延迟设置为 300 毫秒。
再次运行分析。所有网络资源的“发送请求并等待”都应增加到大约 300 毫秒。这将进一步推动 LCP 数字:对我来说是1.2 秒。
现在是有趣的部分:如果我将带宽恢复到超高速但保持低延迟会发生什么?让我们尝试此设置:
如果您的服务器位于挪威某个地方,而客户端是富有的澳大利亚人,则很容易发生这种情况。
这是结果:
LCP 数字约为960 毫秒。它比我们之前尝试过的最慢的互联网速度还要差!在这种情况下,捆绑包大小并不重要,CSS 大小根本不重要。即使您将两者都减半,LCP 指标也几乎不会移动。高延迟胜过一切。
这让我想到了每个人都应该实现的第一个性能改进,如果他们还没有实现的话。它被称为“确保静态资源始终通过 CDN 提供服务”。
CDN(内容分发网络)基本上是任何前端性能相关工作的第 0 步,甚至在开始考虑更花哨的东西(如代码分割或服务器组件)之前。
任何 CDN(内容分发网络)的主要目的是减少延迟并尽快将内容交付给最终用户。它们为此实施了多种策略。本文最重要的两个是“分布式服务器”和“缓存”。
CDN 提供商将在不同的地理位置拥有多个服务器。这些服务器可以存储静态资源的副本,并在浏览器请求它们时将它们发送给用户。CDN 基本上是您原始服务器周围的一个软层,可以保护它免受外部影响并最大限度地减少它与外部世界的交互。它有点像内向者的 AI 助手,它可以在无需让真人参与的情况下处理典型的对话。
在上面的示例中,我们的服务器位于挪威,客户端位于澳大利亚,我们有这样的图片:
有了中间的 CDN,图片就会改变。CDN 将在更靠近用户的地方拥有一个服务器,例如,也在澳大利亚某个地方。在某个时刻,CDN 将从原始服务器接收静态资源的副本。之后,来自澳大利亚或附近任何地方的用户都将获得这些副本,而不是来自挪威服务器的原始副本。
它实现了两个重要的事情。首先,原始服务器上的负载减少了,因为用户不再需要直接访问它。其次,用户现在可以更快地获得这些资源,因为他们不再需要跨越海洋来下载一些 JavaScript 文件了。
而我们上面模拟中的 LCP 值从960 毫秒下降到 640 毫秒?。
到目前为止,我们只讨论了首次访问性能 - 从未访问过您网站的人的性能。但希望该网站如此出色,以至于大多数首次访问者都会变成常客。或者至少他们在第一次加载后不会离开,浏览几个页面,也许会购买一些东西。在这种情况下,我们通常期望浏览器缓存静态资源(如 CSS 和 JS)- 即在本地保存它们的副本,而不是总是下载它们。
让我们看看在这种情况下性能图表和数字如何变化。
再次打开学习项目。在开发工具中,将“网络”设置为我们之前创建的“平均 3G” - 具有高延迟和低带宽,这样我们就可以立即看到差异。并确保“禁用网络缓存”复选框未选中。
首先,刷新浏览器以确保我们正在消除首次访问者的情况。然后刷新并测量性能。
如果您使用的是学习项目,最终结果可能会有些令人惊讶,因为它看起来像这样:
CSS 和 JavaScript 文件在网络选项卡中仍然非常突出,我看到它们在“发送请求并等待”中大约有 300 毫秒 - 我们在“平均 3G”配置文件中设置的延迟设置。结果,LCP 并不像它可能的那样低,并且当浏览器只是等待阻塞 CSS 时,我有一个 300 毫秒的差距。
发生了什么?浏览器不应该缓存这些东西吗?
我们现在需要使用“网络”面板来了解发生了什么。打开它并在那里找到 CSS 文件。它应该如下所示:
这里最有趣的是“状态”列和“大小”。在“大小”中,它绝对不是整个 CSS 文件的大小。它太小了。在“状态”中,它不是我们通常的 200“一切正常”状态,而是不同的东西 - 304 状态。
这里有两个问题 - 为什么是 304 而不是 200,以及为什么根本发送了请求?为什么缓存不起作用?
首先,304 响应。这是一个配置良好的服务器为条件请求发送的响应 - 其中响应根据各种规则而变化。此类请求经常用于控制浏览器缓存。
例如,当服务器接收到 CSS 文件的请求时,它可以检查上次修改文件的时间。如果此日期与浏览器端缓存文件中的日期相同,则它将返回带有空正文的 304(这就是为什么它只有 223 B)。这表示浏览器可以安全地重新使用它已经拥有的文件。无需浪费带宽并再次重新下载它。
这就是为什么我们在性能图片中看到很大的“发送请求并等待”数字 - 浏览器要求服务器确认 CSS 文件是否仍然是最新的。这就是为什么那里的“内容下载”是 0.33 毫秒 - 服务器返回“304 未修改”,浏览器只是重新使用了之前下载的文件。
现在,对于第二个问题 - 为什么根本发送了这个请求?
此行为由服务器设置为响应的 Cache-Control 标头控制。单击“网络”面板中的 CSS 文件以查看请求/响应的详细信息。在“标头”选项卡的“响应标头”块中查找“Cache-Control”值:
在此标头内,可以以不同的组合组合多个指令,用逗号分隔。在我们的例子中,有两个:
因此,基本上,此标头告诉浏览器:
结果,浏览器总是与服务器核实,并且从不立即使用缓存。
但是,我们可以很容易地改变这一点 - 我们只需要将 max-age 数字更改为 0 到 31536000(一年,允许的最大秒数)之间即可。为此,在您的学习项目中,转到 backend/index.ts 文件,找到设置 max-age=0 的位置,并将其更改为 31536000(一年)。刷新页面几次,您应该在“网络”选项卡中看到 CSS 文件的以下内容:
请注意,现在“状态”列已变灰,对于“大小”,我们看到“(内存缓存)”。CSS 文件现在从浏览器的缓存中提供服务,并且一年内将一直如此。刷新页面几次以查看它不会更改。
现在,对于处理缓存标头的全部要点:让我们再次测量页面的性能。不要忘记设置“平均 3G”配置文件设置并保持“禁用缓存”设置未选中。
结果应该类似于:
尽管延迟很高,“发送请求并等待”部分几乎减少到零,“解析 HTML”和 JavaScript 评估之间的差距几乎消失了,我们的 LCP 值又回到了 ~650 毫秒。
上述信息是否意味着缓存是我们的性能灵丹妙药,我们应该尽可能积极地缓存所有内容?绝对不是!除了其他一切之外,创建“不精通技术的客户”和“需要通过电话解释如何清除浏览器缓存”的组合的可能性将导致最资深的开发人员出现恐慌性发作。
有数百万种优化缓存的方法,数百万种在 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 文件。缓存已清除。
查找项目的 dist 文件夹的等效项并运行您的构建命令。
如果您的构建系统就是这样配置的 - 您很幸运。您可以安全地配置服务器以设置生成资产的最大 max-age 标头。如果您同样对所有图像进行版本控制 - 更好的是,您还可以将图像包含到列表中。
根据网站及其用户及其行为,这可能会为您免费提供初始加载的相当不错的性能提升。
此时,您可能正在想类似这样的事情,“你疯了。我周末用 Next.js 构建了一个简单的网站,并在 2 分钟内将其部署到 Vercel/Netlify/HottestNewProvider。当然,这些现代工具会为我处理所有这些。”这很公平。我也这么认为。但后来我实际上检查了一下,哇,我很惊讶?
我的两个项目对 CSS 和 JS 文件都有 max-age=0 和 must-revalidate。事实证明,这是我的 CDN 提供商的默认设置??♀️。当然,他们有理由
以上是React 开发人员的初始负载性能:深入研究的详细内容。更多信息请关注PHP中文网其他相关文章!