对我们的下一本书感兴趣吗?了解更多关于 使用 React 构建大型 JavaScript Web 应用程序

性能模式

优化加载顺序

注意: 本文很大程度上受到了来自 Chrome Aurora 团队 的见解的影响,特别是 Shubhie Panicker,他一直在研究最佳加载顺序。

在每个成功的网页加载中,一些关键组件和资源会在恰当的时间变得可用,从而为您提供流畅的加载体验。这确保用户感知应用程序的性能是出色的。这种出色的用户体验通常也应该转化为通过 核心 Web 指标

用于衡量性能的关键 指标,例如 首次内容绘制最大内容绘制首次输入延迟 等,直接取决于关键资源的加载顺序。例如,如果未加载关键资源(例如 英雄图像),页面将无法显示其 LCP。本文探讨了资源加载顺序与 Web 指标之间的关系。我们的目标是提供有关如何优化加载顺序以获得更好的 Web 指标的明确指导。

在我们确定理想的加载顺序之前,让我们首先尝试了解为什么正确获得加载顺序如此困难。

为什么难以实现最佳加载?

我们有幸能够为我们许多合作伙伴的网站进行性能分析。我们发现跨不同合作伙伴网站,困扰页面高效加载的多个类似问题。

开发人员的期望与浏览器优先处理页面上资源的方式之间通常存在关键差距。这通常会导致次优的性能得分。我们进一步分析以发现导致这种差距的原因,以下几点总结了我们分析的本质。

次优排序

Web 指标 优化不仅需要深入了解每个指标的含义,还需要了解它们的发生顺序以及它们与不同关键资源之间的关系。FCP 发生在 LCP 之前,LCP 发生在 FID 之前。因此,实现 FCP 所需的资源应优先于 LCP 所需的资源,然后是 FID 所需的资源。

资源通常没有按照正确的顺序排序和管道化。这可能是因为开发人员不知道指标对资源加载的依赖关系。因此,相关资源有时无法在相应指标触发时及时提供。

示例:

a) 在 FCP 触发时,英雄图像应该可用以触发 LCP。
b) 在 LCP 触发时,JavaScript (JS) 应该已下载、解析并准备就绪(或正在执行),以解除阻止交互 (FID)。

网络/CPU 利用率

资源也没有得到适当的管道化,以确保充分利用 CPU 和网络。这会导致 CPU 上的“空闲时间”,即进程受网络绑定时,反之亦然。

一个很好的例子是脚本,它们可能是同时或按顺序下载的。由于带宽在并发下载期间被分发,因此所有脚本的总下载时间对于顺序下载和并发下载都是相同的。如果您同时下载脚本,则在下载期间 CPU 的利用率不足。但是,如果您按顺序下载脚本,则 CPU 可以尽快开始处理第一个脚本,因为它已下载。这会导致更好的 CPU 和网络利用率。

第三方 (3P) 产品

3P 库通常需要为网站添加常见的功能和功能。第三方包括广告、分析、社交小部件、实时聊天和其他为网站提供支持的嵌入。第三方库附带自己的 JavaScript、图像、字体等。

3P 产品通常没有动力去优化和支持消费站点加载性能。它们可能具有很高的 JavaScript 执行成本,这会导致交互延迟,或者妨碍其他关键资源的下载。

包含 3P 产品的开发人员可能倾向于更多地关注它们在功能方面的价值,而不是性能影响。因此,3P 资源有时会随意添加,没有充分考虑它如何融入整体加载顺序。这使得它们难以控制和调度。

平台怪癖

浏览器在优先处理请求和实施提示方面可能有所不同。如果您深入了解平台及其怪癖,优化将更容易。特定于特定浏览器的行为使得难以始终如一地实现所需的加载顺序。

一个例子是 Chromium 平台上的预加载错误。可以使用 预加载 (<link rel=preload>) 指令来告诉浏览器尽快下载关键资源。仅当您确定该资源将在当前页面上使用时才应使用它。Chromium 中的错误会导致它表现出这样一种行为,即通过 <link rel=preload> 发出的请求始终在预加载扫描程序看到的其他请求之前开始,即使那些请求具有更高的优先级也是如此。此类问题会给优化计划带来麻烦。

HTTP2 优先级

该协议本身并未提供许多选项或旋钮来调整资源的顺序和优先级。即使提供了更好的优先级原语,仍然存在一些 HTTP2 优先级问题,这 使得最佳排序变得困难。主要的是,我们无法预测服务器或 CDN 将以何种顺序优先处理对单个资源的请求。一些 CDN 重新优先处理请求,而其他 CDN 则实施部分、有缺陷或没有优先级。

资源级优化

有效的排序需要对正在排序的资源进行最佳服务,以便它们能够快速加载。关键 CSS 应该内联,图像应该正确调整大小,JS 应该进行代码拆分并增量交付。

框架本身缺乏允许代码拆分并增量提供 JS 和数据的构造。用户必须依靠以下方法之一来拆分 1P JS 的大块代码

  1. 现代 React (Suspense / 并发模式 / 数据获取) - 这仍然仅供实验使用
  2. 使用 动态导入 进行延迟加载 - 这并不直观,开发人员需要手动识别代码拆分的边界。

进行代码拆分时,开发人员需要实现适当的块粒度,因为存在粒度与性能之间的权衡。

更高的粒度是可取的,因为它

  1. 将单个路线和后续用户交互所需的 JS 最小化
  2. 允许缓存公共依赖项。这确保库中的更改不需要重新获取整个捆绑包。

同时,代码拆分时过高的粒度可能会很糟糕,因为太多的小块会降低单个块的压缩率并影响浏览器性能。

资源优化还需要消除死代码或未使用代码。不必要的或过时的 JS 可能会经常发送到现代浏览器,这会对性能产生负面影响。转译成 ES5 并与 polyfill 捆绑在一起的 JS 对现代浏览器来说是没必要的。库和 npm 包通常不会以 ES 模块格式发布。这使得捆绑程序难以进行树摇和优化

您可能已经注意到,这些问题并不局限于特定的一组资源或平台。为了解决这些问题,需要了解整个技术堆栈以及如何将不同的资源合并在一起以获得最佳指标。在我们定义整体优化策略之前,让我们看一下单个资源要求是如何违背我们的目的的。

更多关于资源 - 关系、约束和优先级

在上一节中,我们提供了一些示例,说明某些资源是如何为特定事件(如 FCP 或 LCP 触发)所必需的。在我们讨论处理它们的方法之前,让我们先尝试了解所有此类依赖关系。以下是按资源列出的建议、约束和注意事项,在定义理想顺序之前需要考虑这些因素。

关键 CSS

关键 CSS 指的是实现 FCP 所需的最小 CSS。最好将此类 CSS 内联在 HTML 中,而不是从另一个 CSS 文件导入它。任何给定时间都应仅下载路由所需的 CSS,并且所有关键 CSS 应相应地进行代码拆分。

如果无法内联,关键 CSS 应该预加载并从与文档相同的来源提供。避免从多个域提供关键 CSS,或直接使用第三方关键 CSS(如 Google 字体)。您自己的服务器可以作为第三方关键 CSS 的代理。

获取 CSS 的延迟或获取 CSS 的顺序不正确可能会影响 FCP 和 LCP。为了避免这种情况,非内联 CSS 应该优先于网络上的 1P JS 和 ATF 图像。

内联的 CSS 太多会导致 HTML 膨胀和主线程上的样式解析时间过长。这会损害 FCP。因此,识别什么是关键的以及代码拆分至关重要。

内联 CSS 无法被缓存。解决此问题的一种方法是为 CSS 创建一个可以缓存的重复请求。但是请注意,这可能会导致多个完整页面布局,从而影响 FID。

字体

与关键 CSS 一样,关键字体 的 CSS 也应该内联。如果无法内联,则应使用指定的 preconnect 加载脚本。获取字体(例如,Google 字体或来自不同域的字体)的延迟会影响 FCP。Preconnect 会告诉浏览器更早地建立到这些资源的连接。

内联字体会导致 HTML 体积明显增大,并延迟启动其他关键资源的获取。字体回退 可用于解除 FCP 阻塞并使文本可用。但是,使用字体回退会影响 CLS,因为字体会发生跳动。它还会影响 FID,因为当真正的字体到达时,主线程上可能会出现大量的样式和布局任务。

页面可见区域内的图片

这是指用户在页面加载时最初可以看到的图片,因为它们位于视窗内。页面英雄图片是 ATF 图片的一个特例。所有 ATF 图片都应该进行尺寸设置。未设置尺寸的图片会导致 CLS 指标下降,因为在它们完全呈现时会发生布局偏移。ATF 图片的占位符应该由服务器渲染。

延迟的英雄图片或空白占位符会导致 LCP 延迟。此外,如果占位符尺寸与实际英雄图片的内在尺寸不匹配,并且图片没有覆盖替换,则 LCP 会重新触发。理想情况下,ATF 图片不会影响 FCP,但在实际情况下,图片可能会触发 FCP。

页面不可见区域内的图片

这是指用户在页面加载时无法立即看到的图片。因此,它们是延迟加载的理想候选者。这确保了它们不会与页面上需要的 1P JS 或重要的 3P 争夺资源。如果 BTF 图片在 1P JS 或重要的 3P 资源之前加载,FID 会延迟。

1P JavaScript

1P JS 会影响应用程序的交互就绪状态。它可能会在网络上被图片和 3P JS 延迟,在主线程上被 3P JS 延迟。因此,它应该在网络上 ATF 图片之前开始加载,并在主线程上 3P JS 之前执行。在服务器端渲染的页面中,1P JS 不会阻塞 FCP 和 LCP。

3P JavaScript

HTML 头部中的 3P 同步脚本可能会阻塞 CSS 和字体解析,从而影响 FCP。头部中的同步脚本还会阻塞 HTML 主体解析。3P 脚本在主线程上的执行会延迟 1P 脚本的执行,并推迟水合和 FID。因此,需要更好地控制 3P 脚本的加载。

这些建议和约束通常适用于任何技术栈和浏览器。请注意,有些建议也会成为约束。例如,内联字体和 CSS 很好,但太多会导致膨胀。诀窍是在“太少太晚”和“太多太早”之间找到平衡。

下表让我们了解 Chrome 加载不同资源的优先级。结合有关优先级的知识和对资源类型的讨论,将有助于我们更好地理解下一节中提出的加载顺序。

image

以下是从该表格中获得的主要结论。

  • CSS 和字体以最高优先级加载。这应该有助于我们优先考虑关键 CSS 和字体。
  • 脚本根据它们在文档中的位置以及它们是异步、延迟还是阻塞来获得不同的优先级。在第一个图片(或文档中较早的图片)之前请求的阻塞脚本优先级高于在第一个图片获取后请求的阻塞脚本。无论异步/延迟/注入脚本在文档中的位置如何,它们的优先级都最低。因此,我们可以通过对 async 和 defer 使用适当的属性来优先考虑不同的脚本。
  • 可见且位于视窗内的图片优先级高于不在视窗内的图片(网络:最低)。这有助于我们优先考虑 ATF 图片而不是 BTF 图片。

现在让我们看看如何将所有这些细节组合在一起,以定义最佳加载顺序。

理想的加载顺序是什么

有了这些背景知识,我们现在可以提出一个加载顺序,该顺序应该优化 1P 和 3P 资源的加载。建议的顺序使用 Next.js 服务器端渲染 (SSR) 作为优化的参考。

当前状态

根据我们的经验,以下是我们观察到的 Next.js SSR 应用程序在优化之前典型的加载顺序。

CSSCSS 在 JS 之前预加载,但未内联
JavaScript

1P JS 预加载

3P JS 未管理,仍然可能在文档中的任何位置阻塞渲染。

字体

字体既未内联,也未使用 preconnect

字体通过外部样式表加载,这会延迟加载

字体可能阻塞显示,也可能不阻塞显示。

图片

英雄图片没有优先级

ATF 和 BTF 图片均未优化

以下是我们合作伙伴网站之一的此类顺序的示例。加载顺序的优点和缺点作为注释包含在内。

image

不含 3P 的建议顺序

以下是考虑了之前讨论的所有约束的加载顺序。让我们先处理不含 3P 的顺序。然后,我们将看到如何在此顺序中插入 3P 资源。请注意,我们在这里将 Google 字体视为 1P。

主浏览器线程上的事件顺序

网络上的请求顺序。

1解析 HTML小型内联 1P 脚本。1
2执行小型内联 1P 脚本内联关键 CSS(如果外部,则预加载)2
内联关键字体(如果外部,则预连接)3
3解析 FCP 资源(关键 CSS、字体)LCP 图片(如果外部,则预连接)4
首屏内容绘制 (FCP)字体(从内联字体 CSS(预连接)触发)5
4渲染 LCP 资源(英雄图片、文本)非关键(异步)CSS6
用于交互的 First-party JS7
页面可见区域内的图片(预连接)8
最大内容绘制 (LCP)

页面不可见区域内的图片

 

9

5渲染重要的 ATF 图片
视觉完整
6解析非关键(异步)CSS
7执行 1P JS 并水合

延迟加载的 JS 代码块

10
首屏输入延迟 (FID)

虽然此顺序的某些部分可能是直观的,但以下几点将有助于进一步说明。

  1. 我们建议尽可能避免预加载,因为它会强制对每个前面的资源进行手动预加载,还会导致手动管理顺序。预加载应该特别避免用于字体,因为它很难检测关键字体。
  2. 字体 CSS 应该理想地内联。来自其他来源的字体应该使用 preconnect 获取。
  3. 建议对所有来自其他来源的资源使用 preconnect。这将确保预先建立连接以下载这些资源。
  4. 非关键 CSS 应该在用户交互开始之前获取(FID)。这将避免由于随后呈现此类 CSS 而导致的样式问题。
  5. 在网络上 ATF 图片之前开始获取 First-party JS。下载和解析 JS 需要一些时间。
  6. 在解析 1P JS 的同时,可以在主线程上并行进行 HTML 解析和 ATF 图片下载。

包含 3P 的建议顺序

最后,我们已经到了可以为现代 Web 应用程序中通常加载的所有关键资源提出顺序的阶段。以下是在图片中包含 3P 资源的情况下,主浏览器线程上的事件顺序和网络获取请求顺序。

事件顺序请求顺序说明
1解析 HTML阻塞 FCP 的 3P 资源
2
3小型内联 1P 脚本。
4执行小型内联 1P 脚本内联关键 CSS(如果外部,则预加载)
5解析阻塞 FCP 的 3P 资源内联关键字体(如果外部,则预连接)
6解析 FCP 资源(关键 CSS、字体)用于 LCP 的个性化 3P ATF 图片
7首屏内容绘制 (FCP)LCP 图片(如果外部,则预连接)
8渲染用于 LCP 的个性化 3P ATF 图片字体(从内联字体 CSS(预连接)触发)
9非关键(异步)CSS
10渲染 LCP 资源(英雄图片、文本)必须在第一次用户交互之前执行的 3P
11用于交互的 First-party JS
12最大内容绘制 (LCP)
13渲染重要的 ATF 图片默认 3P JS
14解析非关键(异步)CSS
15执行第一次用户交互所需的 3P页面不可见区域内的图片
16执行 1P JS 并水合延迟加载的 JS 代码块
首屏输入延迟 (FID)不太重要的 3P JS

这里的主要问题是如何确保 3P 脚本以最佳方式下载,并按所需顺序下载。

由于脚本请求转到另一个域,因此建议对以下 3P 请求使用 preconnect。这有助于优化下载。

  • #1 - 阻塞 FCP 的 3P 资源
  • #5 - 用于 LCP 的个性化 3P ATF 图片
  • #9 - 必须在第一次用户交互之前执行的 3P
  • #12 - 默认 3P JS

为了实现所需的顺序,我们建议使用 Next 的 ScriptLoader 组件。此组件旨在“优化关键渲染路径,并确保外部脚本不会成为页面最佳加载的瓶颈”。与我们的讨论最相关的功能是加载优先级。这使我们能够在不同的里程碑处安排脚本,以支持不同的用例。以下是可用的加载优先级值

After-Interactive:在下次水合之后加载特定的 3P 脚本。这可以用来加载我们希望尽快执行,但在 1P 脚本之后执行的标签管理器、广告或分析脚本。

Before-Interactive:在水合之前加载特定的 3P 脚本。它可用于我们希望 3P 脚本在 1P 脚本之前执行的情况。例如,polyfill.io、机器人检测、安全和身份验证、用户同意管理(GDPR)等。

Lazy-Onload:将所有其他资源优先于指定的 3P 脚本,并延迟加载脚本。它可以用于 CRM 组件,如 Google 反馈,或社交网络特定脚本,如用于分享按钮、评论等脚本。

因此,preconnect、脚本属性和 Next.js 的 ScriptLoader 可以帮助我们获得所有脚本的所需顺序。

结论

优化应用程序的责任落在使用的平台创建者和使用它的开发人员的肩上。需要解决常见问题。我们的目标是让内部向外的排序变得更容易。经过测试和验证的一套针对不同用例的建议,以及像 Script Loader 这样的计划,有助于在 React-Next.js 栈中实现这一点。下一步是确保新的应用程序符合上述建议。

特别感谢 Leena Sohoni(技术分析师/作家),感谢她对本文的所有贡献。