load
优化加载顺序 Optimize your loading sequence
每次成功加载网页时,某些关键组件和资源都会在合适的时间点可用,为您提供流畅的加载体验。这确保了用户会认为应用程序的性能非常出色。这种出色的用户体验通常也会反映在通过核心网络指标上。
用于衡量性能的关键指标,如首次内容绘制、最大内容绘制、首次输入延迟等,直接依赖于关键资源的加载序列。例如,如果像英雄图像这样的关键资源没有加载完成,页面就无法完成其最大内容绘制。本文将探讨资源加载顺序与网页核心指标之间的关系。我们的目标是提供关于如何优化加载顺序以改善网页核心指标的明确指导。
在我们确定理想的加载顺序之前,让我们先试着理解为什么正确的加载顺序如此难以确定。
为何难以实现最佳加载?
开发者预期和浏览器优先处理页面资源的方式之间通常存在一个关键差距,这常常导致性能得分不尽人意。我们进一步分析,发现了造成这种差距的原因,以下是分析的要点总结。
- 非最佳排序
Web 性能优化的关键不仅在于充分了解每个指标的含义,还在于它们发生的顺序以及与不同关键资源的关联关系。FCP 发生在 LCP 之前,FID 发生在 LCP 之前。因此,实现 FCP 所需的资源应优先于 LCP 所需的资源,然后是 FID 所需的资源。 资源的顺序往往没有安排妥当,管道化也没有以正确的顺序进行。这可能是因为开发者不了解指标对资源加载的依赖关系。因此,相关资源有时无法在相应指标触发时及时可用。
示例:
a 当 FCP 触发时,主图应该可用于触发 LCP。
b 当 LCP 触发时,JavaScript(JS)应该已经下载、解析并准备就绪(或正在执行),以便取消阻止交互(FID)
网络/CPU 利用率
资源也没有适当地进行管道化处理,以确保 CPU 和网络的完全利用。这导致 CPU 在进程受网络限制时出现“空闲时间”,反之亦然。
一个很好的例子是可能并发或顺序下载的脚本。在并发下载时,带宽会被分割,因此无论是顺序下载还是并发下载,下载所有脚本的总时间是一样的。如果并发下载脚本,那么在下载过程中 CPU 的利用率较低。但是,如果按顺序下载脚本,则可以在下载第一个脚本后立即开始由 CPU 进行处理。这实现了更好的 CPU 和网络利用率。
- 第三方产品
第三方库通常用于向网站添加常见功能和特性。第三方包括广告、分析、社交插件、实时聊天和其他支持网站运行的嵌入内容。第三方库有自己的 JavaScript、图像、字体等。
第三方产品通常没有动力优化并支持消费者网站的加载性能。它们可能会产生高昂的 JavaScript 执行成本,延迟交互,或阻碍其他关键资源的下载。
使用第三方产品的开发人员可能更多地关注它们所增加的功能价值,而不是性能影响。因此,第三方资源有时会随意添加,而没有全面考虑如何融入整体的加载序列。这使得它们难以控制和安排。
- 平台特性
浏览器在如何优先处理请求和实现提示方面可能存在差异。如果你深入了解平台和其特性,优化将更容易。特定浏览器的特定行为使得难以持续实现所需的加载序列。
一个例子是 Chromium 平台上的预加载错误。预加载(<link rel=preload>
)指令可用于告诉浏览器尽快下载关键资源。只有当你确定该资源将在当前页面上使用时才应使用它。
Chromium 中的错误导致其行为表现为通过<link rel=preload>
发出的请求总是在预加载扫描器看到的其他请求之前开始,即使后者的优先级更高。这类问题会对优化计划造成阻碍。
- HTTP2 优先级设置
该协议本身并没有提供许多选项或工具来调整资源的顺序和优先级。即使提供了更好的优先级设置工具,HTTP2 的优先级设置也存在一些基本问题,使得难以进行最优排序。主要是我们无法预测服务器或 CDN 将为个别资源请求的优先级顺序。一些 CDN 会重新调整请求的优先级,而其他则实施部分、有缺陷或不设置优先级的策略。
- 资源层面优化
有效的排序需要所排序的资源以最优化方式提供,以便快速加载。关键 CSS 应内联,图像大小应合适,JS 应进行拆分并增量提供。
框架本身缺乏允许代码拆分并增量提供 JS 和数据的结构。用户必须依赖以下方法之一来拆分大量的 IP JS
现代 React(Suspense/并发模式/数据获取)-这仍然只可用于实验。 使用动态导入进行懒加载-这不太直观,开发者需要手动确定拆分代码的范围。 在进行代码拆分时,开发者需要达到合适的粒度,因为粒度与性能之间存在权衡关系。
更高的粒度是可取的,因为它:
最小化单个路由和后续用户交互所需的 JS;允许缓存常见的依赖项。这确保了库中的更改不需要重新获取整个捆绑包;同时,当代码粒度过高时,可能会产生不利影响。因为太多的小块会降低单个块的压缩率并影响浏览器性能。资源优化还需要消除死代码或未使用的代码。不必要的或过时的 JS 可能会经常发送到现代浏览器,这会对性能产生负面影响。对于现代浏览器来说,将 JS 转译为 ES5 并使用 polyfills 捆绑是不必要的。库和 npm 包通常不以 ES 模块格式发布。这使得捆绑器难以摇树和优化。
正如您可能已经注意到的,这些问题并非局限于特定的资源集或平台。为了解决这个问题,我们需要对整个技术栈有深入的了解以及如何将不同的资源合并起来以达到最佳指标。在我们定义整体优化策略之前,让我们先看看个别资源需求是如何妨碍我们的目标的。
关于资源的相关介绍 —— 关系、限制与优先级设定
在前一章节中,我们给出了一些示例,说明了特定资源对于特定事件(如首次内容绘制或最大内容绘制)如何起到必要作用。在我们讨论如何处理这些依赖关系之前,让我们先了解所有这样的依赖关系。以下是在定义理想序列之前需要考虑的资源方面的建议、限制和需要注意的事项。
关键 CSS(Critical 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 来加载脚本。延迟获取字体(例如,谷歌字体或来自不同域的字体)会影响 FCP(首屏完成时间)。Preconnect 告诉浏览器尽早建立对这些资源的连接。
内联字体可能会使 HTML 显著膨胀并延迟启动其他关键资源的获取。可以使用备用字体来阻止 FCP 阻塞并使文本可用。然而,使用备用字体可能会影响 CLS(累计布局稳定性),因为字体跳跃。此外,当实际字体到达时,它可能会在主线程上产生潜在的大样式和布局任务从而影响 FID(首屏输入延迟)。
折叠上方的图片(ATF)
这指的是在页面加载时最初可见给用户的图片,因为它们位于视口中。ATF 图片的特例是页面的主要图片。所有 ATF 图片都应调整大小。未调整大小的图片会损害 CLS 指标,因为它们完全渲染时会出现布局移动。ATF 图片的占位符应由服务器呈现。
延迟显示主要图片或空占位符会导致延迟 LCP(Largest Contentful Paint)。另外,如果占位符的大小与实际主要图片的固有尺寸不匹配且图片未被替换覆盖,则 LCP 会重新触发。理想情况下,ATF 图片不应影响 FCP(First Contentful Paint),但在实践中,图片可以触发 FCP。
- Below the Fold (BTF) Images(折叠以下图片)
这些图片在页面加载时不会立即显示给用户。因此,它们非常适合懒加载。这确保了它们不会与第一方 JavaScript 或页面所需的重要第三方资源竞争。如果先在第一方 JavaScript 或重要的第三方资源之前加载折叠图片,首交互延迟(FID)将会受到影响。
- 1P JavaScript(第一方 JavaScript)
第一方 JavaScript 影响应用程序的交互就绪性。它可能会在图片和第三方 JavaScript 之后的网络上延迟,以及在第三方 JavaScript 之后的主线程上延迟。因此,它应该在网络上先于 ATF 图片开始加载,并在主线程上先于第三方 JavaScript 执行。第一方 JavaScript 不会阻止服务器端渲染的页面的 FCP 和 LCP。
- 3P JavaScript(第三方 JavaScript)
HTML 头部中的第三方同步脚本可能会阻止 CSS 和字体解析,从而影响首屏完成时间(FCP)。头部中的同步脚本也会阻止 HTML 主体解析。主线程上的第三方脚本执行可能会延迟第一方脚本的执行并推迟渲染和首交互延迟(FID)。因此,需要更好地控制第三方脚本的加载。
这些建议和约束通常不受技术栈和浏览器的影响。请注意,一个建议也可能成为一个约束。例如,内联字体和 CSS 很棒,但过多的内联可能会导致页面过于庞大。关键是要在“太少太慢”和“太多太快”之间找到平衡。
以下图表帮助我们理解 Chrome 加载不同资源的优先级。结合关于优先级的信息和关于资源类型的讨论,将有助于更好地理解下一部分提出的加载序列。
以下是此表的关键要点:
- CSS 和字体具有最高优先级加载。这将有助于我们优先处理关键的 CSS 和字体。
- JavaScript 根据它们在文档中的位置以及它们是否为异步、延迟或阻塞的不同而具有不同的优先级。
- 在第一张图片请求之前请求的阻塞脚本的优先级高于第一张图片获取后请求的阻塞脚本。
- 无论异步脚本或延迟脚本在文档中的位置如何,它们的优先级都是最低的。因此,我们可以通过使用适当的异步和延迟属性来优先处理不同的脚本。
- 对于可见且位于视口的图像具有较高的优先级(网络:中),那些不在视口中的图像优先级较低(网络:最低)。这有助于我们优先处理 ATF 图像而非 BTF 图像。现在让我们看看如何整合上述所有细节来定义最佳的加载顺序。
理想的加载顺序 What is the Ideal Loading Sequence
有了这个背景,我们现在可以提出一个优化加载序列,该序列应该可以优化第一方和第三方资源的加载。所提议的序列以 Next.js 服务器端渲染(SSR)作为优化的参考。
当前状态: 基于我们的经验,在优化之前,以下是我们在 Next.js SSR 应用程序中观察到的典型加载顺序。
无第三方资源的建议优化顺序
以下是考虑到之前讨论的所有约束条件的加载序列。让我们先解决一个没有第三方资源(3P)的序列。然后,我们将看看如何在此序列中插入第三方资源。请注意,在这里我们将 Google Fonts 视为 1P 资源。
虽然此序列的一些部分可能是直观的,但以下几点将有助于进一步证明其合理性:
我们建议您尽可能避免预加载,因为预加载会强制对所有先前的资源进行手动预加载,并导致顺序的手动整理。对于字体,尤其应避免使用预加载,因为检测关键字体比较困难。
字体 CSS 应理想地进行内联。来自其他来源的字体应通过 preconnect 获取。
建议对所有来自其他来源的资源使用 preconnect。这将确保提前建立连接,以便下载这些资源。
非关键 CSS 应在用户交互开始之前(FID)加载,以避免因后续渲染这些 CSS 而导致的样式问题。
在网络上下载 ATF 图像之前,首先应开始获取第一方 JS,因为下载和解析该 JS 会花费一些时间。
在解析第一方 JS 的同时,主线程可以继续解析 HTML 并下载 ATF 图像,二者可以并行进行。
带第三方资源的建议优化顺序
... 省略 建议看原文
https://www.patterns.dev/vanilla/loading-sequence
预请求 Prefetch
通过 <link rel="prefetch">
实现,是一种浏览器优化技术,它使我们能够在真正需要时为后续路由或页面提前获取所需的资源。预加载可以通过多种方式实现。可以在 HTML 中进行声明式实现(如下例所示),也可以通过 HTTP 标头( Link: </js/chat-widget.js>; rel=prefetch
),或通过 Service Workers,或者通过更自定义的方式(如 Webpack)。
<link
rel="prefetch"
href="/pages/next-page.html"
/>
<link
rel="prefetch"
href="/js/emoji-picker.js"
/>
2
3
4
5
6
7
8
在许多情况下,我们知道用户在页面初次渲染后会很快请求某些资源。虽然这些资源可能不会立即显示,因此不应该包含在初始捆绑包中,但尽可能地减少加载时间对于提供更好的用户体验而言是非常重要的!
我们知道可能会在应用中的某个时刻使用的组件或资源可以预先获取。我们可以通过给导入语句添加一条魔法注释来告诉 Webpack 某些捆绑包需要预先获取:/_ webpackPrefetch: true _/。
const EmojiPicker = import(/* webpackPrefetch: true */ './EmojiPicker')
构建应用后,我们可以看到 EmojiPicker
将被预加载。
实际输出在文档的 <head>
中显示为带有 rel="prefetch"
的 <link>
标签。
<link
rel="prefetch"
href="emoji-picker.bundle.js"
as="script"
/>
<link
rel="prefetch"
href="vendors~emoji-picker.bundle.js"
as="script"
/>
2
3
4
5
6
7
8
9
10
预取模块是用户在请求资源之前,由浏览器请求并加载的。当浏览器处于空闲状态并计算出有足够的带宽时,它会发出请求以加载资源并将其缓存。由于我们不必在用户点击按钮后等待请求完成,因此拥有缓存的资源可以大大缩短加载时间。它可以从缓存中获取已加载的资源。
尽管预取是优化加载时间的好方法,但不要过度使用。如果用户最终没有请求 EmojiPicker 组件,我们就不必加载该资源。这可能会消耗用户的金钱或减慢应用程序的速度。只预取必要的资源。
其他参考
https://www.patterns.dev/vanilla/prefetch
Preload 预加载
<link rel="preload">
是一种浏览器优化,允许提前请求关键资源(这些资源可能发现较晚)。如果您能够很好地手动安排关键资源的加载顺序,这将对提高加载性能和核心网络基本指标产生积极影响。尽管如此,Preload 并非万能灵药,也需要考虑一些权衡。
当针对交互时间或首次输入延迟等指标进行优化时,preload 可以用于加载对于交互必要的 JavaScript 捆绑包(或块)。需要注意的是,在使用 preload 时要特别小心,以避免以延迟首屏内容(如大图像或字体)等资源加载的代价来改善交互性。当试图优化一方 JavaScript 的加载时,也可以在文档的头部(head)或尾部(body)中使用延迟加载(<script defer>
),以帮助早期发现这些资源。
在单页应用程序中的预加载
尽管预取是一种很好的缓存资源方式,这些资源可能在不久的将来被请求,但我们可以即时加载所需的资源。也许这是初始渲染时使用的特定字体,或者是用户立即看到的某些图像。
假设我们的 EmojiPicker 组件应该在初始渲染时立即显示。虽然它不应该包含在主要捆绑包中,但它应该并行加载。就像预取一样,我们可以添加一个魔法注释,让 Webpack 知道这个模块应该被预加载。
const EmojiPicker = import(/* webpackPreload: true */ './EmojiPicker')
预加载的 EmojiPicker 可以与初始捆绑包并行加载。与 prefetch 不同,浏览器仍然可以决定是否认为它具有足够好的互联网连接和带宽来实际预取资源,而预加载的资源无论如何都会被加载。我们不必等到初始渲染后加载 EmojiPicker,资源会立即为我们所用!
当我们以更智能的顺序加载资产时,初始加载时间可能会根据用户的设备和互联网连接状况而有很大变化。只预加载初始渲染后约一秒钟内必须可见的资源。
Preload + the async hack
如果你想让浏览器以高优先级下载脚本,但不想阻止解析器等待脚本加载,你可以使用下面的 preload+async 技巧。在这种情况下,其他资源的下载可能会被 preload 延迟,但这是开发者必须做出的权衡:
<link rel="preload" href="emoji-picker.js" as="script">
<script src="emoji-picker.js" async>
2
Preload in Chrome 95+
- 将其放入 HTTP 头部将优先于其他所有内容加载。
- 一般来说,对于>=Medium 的内容,Preload 将按照解析器获取它们的顺序进行加载,所以在 HTML 开头放置 Preload 时要小心。
- 字体 Preload 最好放在 head 的末尾或 body 的开始部分。
- 导入 Preload 应在需要导入的脚本标签之后进行(这样实际的脚本会先加载/解析)。
- 图像 Preload 优先级较低,应根据异步脚本和其他低优先级标签的顺序进行排序。
结论
再次提醒,要适度使用预加载并始终在生产环境中衡量其影响。如果图像预加载出现在文档的前面,这有助于浏览器发现它(并根据其他资源排序)。使用不当的预加载会导致您的图像延迟首屏渲染(例如 CSS、字体),适得其反。另外,请注意,为了使重新优先级的操作有效,这也取决于服务器是否能正确地优先处理请求。对于在不执行脚本的情况下获取脚本的需求,您可能会发现使用<link rel="preload">
会有所帮助。
PRPL 模式
让我们的应用程序实现全球访问可能会面临挑战!我们必须确保应用程序在低端设备和互联网连接不佳的地区也能表现出良好的性能。为了确保我们的应用程序在困难条件下能够尽可能高效地进行加载,我们可以使用 PRPL 模式。
PRPL 模式主要关注四个性能方面的考虑:
有效地推送关键资源,尽量减少与服务器的往返次数,并缩短加载时间。 尽快渲染初始路由,以提高用户体验。 在后台预先缓存经常访问的路由的资产,以减少对服务器的请求次数,并实现更好的离线体验。 对于不经常请求的路由或资产进行懒加载。
具体来说,PRPL 模式包括以下几点:
- Push(推送):通过优化资源加载策略,减少与服务器的通信次数和往返时间,从而提高页面加载速度。这包括使用 HTTP/2 协议进行资源推送等。
- Render(渲染):尽早渲染初始页面或路由,使用户更快地看到内容并与之互动。这可以通过使用服务端渲染(SSR)等技术实现。
- Pre-cache(预缓存):预先在客户端缓存一些资源或数据,以便在用户再次访问时能够快速加载。这可以通过使用 Service Workers 等技术实现。
- Lazy Load(懒加载):对于非关键资源或低频请求的路由或资产进行懒加载,以减轻初始加载时的负担并提高性能。这可以通过使用 JavaScript 的异步加载技术实现。
详细信息
当我们想要访问一个网站时,我们首先必须向服务器发送请求以获取这些资源。入口点指向的文件从服务器返回,这通常是我们的应用程序的初始 HTML 文件!浏览器中的 HTML 解析器开始解析此数据,一旦开始从服务器接收数据。如果解析器发现还需要更多的资源,如样式表或脚本,则会向服务器发送另一个 HTTP 请求以获取这些资源!
反复请求资源并不理想,因为我们正在试图最小化客户端和服务器之间的往返次数!
很长一段时间以来,我们一直使用 HTTP/1.1 在客户端和服务器之间进行通信。虽然 HTTP/1.1 相对于 HTTP/1.0 引入了许多改进,例如能够在新的 HTTP 请求发送之前保持客户端和服务器之间的 TCP 连接处于活动状态,但仍有一些问题需要解决!
与 HTTP/1.1 相比,HTTP/2 引入了一些重大变化,使我们更容易优化客户端和服务器之间的消息交换。
HTTP/1.1 在请求和响应中使用换行分隔的纯文本协议,而 HTTP/2 将请求和响应分割成较小的片段,称为帧。包含标题和正文字段的 HTTP 请求被分割成至少两个帧:一个标题帧和一个数据帧!
HTTP/1.1 中客户端和服务器之间的 TCP 连接最大数量为 6 个。在新的请求发送到同一 TCP 连接之前,必须先解决前一个请求!如果前一个请求需要很长时间才能解决,这个请求会阻止发送其他请求。这个常见的问题被称为行首阻塞,并会增加某些资源的加载时间!
HTTP/2 利用双向流,使得单个 TCP 连接可以包含多个双向流,客户端和服务器之间可以传输多个请求和响应帧!
服务器一旦接收到特定请求的所有请求帧,就会重新组装它们并生成响应帧。这些响应帧被发送回客户端进行重组。由于流是双向的,我们可以在同一个流上发送请求和响应帧。
HTTP/2 通过允许在之前的请求解决之前在同一 TCP 连接上发送多个请求,解决了头阻塞问题!
HTTP/2 还引入了一种更优化的数据获取方式,称为服务器推送。服务器不必每次都通过发送 HTTP 请求来明确请求资源,而是可以自动发送附加资源,通过“推送”这些资源。
客户收到额外的资源后,这些资源会被存储在浏览器缓存中。当解析入口文件时发现这些资源时,浏览器可以从缓存中快速获取资源,而无需向服务器发送 HTTP 请求!
虽然推送资源可以减少接收额外资源的时间,但服务器推送并不知道 HTTP 缓存!下次访问网站时,我们无法获得已推送的资源,必须再次请求这些资源。为了解决这一问题,PRPL 模式在初始加载后使用服务工作者来缓存这些资源,以确保客户端不会发出不必要的请求。
作为网站作者,我们通常知道哪些资源需要早期获取,而浏览器会尽力猜测这一点。幸运的是,我们可以通过向关键资源添加预加载资源提示来帮助浏览器!
通过告诉浏览器你想要预加载某个资源,你是在告诉浏览器你希望比浏览器自己发现它的时间更早地获取它!预加载是优化对当前路由至关重要的资源加载时间的一个很好的方式。
虽然预加载资源是减少往返次数并优化加载时间的一种很好的方式,但推送太多文件可能是有害的。浏览器缓存是有限的,通过请求客户端实际上不需要的资源可能会不必要地使用带宽。
PRPL 模式专注于优化初始加载。在初始路由完全加载和呈现之前,不会加载任何其他资源!
我们可以通过将应用程序分割成小型、高性能的捆绑包来实现这一点。这些捆绑包应使用户能够在需要时只加载他们所需的资源,同时最大限度地提高可缓存性!
缓存较大的捆绑包可能是一个问题。可能会发生多个捆绑包共享相同资源的情况。
浏览器很难识别捆绑包中的哪些部分在多条路由之间共享,因此无法缓存这些资源。缓存资源对于减少到服务器的往返次数以及使我们的应用程序支持离线使用非常重要!
在使用 PRPL 模式时,我们需要确保请求的捆绑包包含当时所需的最少资源,并且可以被浏览器缓存。在某些情况下,这可能意味着完全不使用捆绑包会更具性能优势,我们可以简单地使用非捆绑模块!
通过配置浏览器和服务器以支持 HTTP/2 推流并有效地缓存资源,可以轻松地模拟通过捆绑应用程序动态请求最少资源的能力所带来的好处。对于不支持 HTTP/2 服务器推流的浏览器,我们可以创建一个优化构建,以尽量减少往返次数。客户端无需知道它接收的是捆绑资源还是非捆绑资源:服务器为每台设备提供相应的构建。
PRPL 模式通常使用应用外壳作为其主入口点,这是一个包含应用程序大部分逻辑的最小文件,并且在路由之间共享!它还包含应用程序的路由器,可以动态请求必要的资源。
PRPL 模式确保在用户的设备上初始路由可见之前,不会请求或呈现其他资源。一旦成功加载了初始路由,就可以安装一个服务器工作器以便在后台获取其他经常访问的路由的资源!
由于这些数据是在后台获取的,用户不会遇到任何延迟。如果用户想要导航到由服务工作者缓存的经常访问的路由,服务工作者可以从缓存中快速获取所需资源,而不是必须向服务器发送请求。
对于不太经常访问的路由的资源可以动态导入。
优化第三方加载 Optimize loading third-parties
在现代网站上很难找到孤立运营的网站。大多数网站共存并依赖于网络上的其他来源的数据、功能、内容和更多内容。您的网站位于另一个域上并由您的网站使用的任何资源都是第三方资源。
网站上包含的典型第三方资源包括:
- 地图、
- 视频、
- 社交媒体和聊天服务的嵌入物、
- 广告、
- 分析组件和标签管理器、
- AB 测试和个性化脚本以及用于提供诸如数据可视化或动画等即用型辅助函数的实用程序库
- 用于机器人检测的 reCAPTCHA 或 CAPTCHA。
您可以使用第三方来集成其他功能,为您的内容增值或减轻从头开始构建网站所涉及的繁琐工作。
虽然第三方资源可以为您的网站增加有价值的功能,但如果出现以下情况,它们也可能会降低网站速度:
- 它们导致每个所需资源都需要前往第三方域进行额外的往返行程。
- 它们大量使用 JavaScript(影响下载和执行时间),或者由于未优化的图像/视频而体积庞大。
- 个人网站所有者无法影响实施,且其行为可能不可预测。
- 它们可能会阻止页面上其他关键资源的渲染并影响核心网络指标(CWV)。
尽管存在这些问题,第三方资源仍可能对企业的业务至关重要。如果您无法摆脱第三方资源,那么最好的选择就是优化它们以减少对性能的影响,这就是我们将在本节中介绍的内容。
我们包含了一些适用于不同类型第三方脚本的策略和最佳实践。Next.js 脚本组件中融入了许多这些最佳实践,您可以在本文后半部分了解相关内容。首先让我们看看如何找出第三方脚本是否正在损害页面性能。
评估 3P 资源的性能影响
您可以使用各种技术来了解第三方代码如何影响您的网站。 以下 Lighthouse 审计有助于识别影响 CWV 的慢第三方脚本。
- 减少阻塞主线程的第三方代码的影响。
- 减少执行时间长的脚本的 JavaScript 执行时间。
- 避免为大脚本带来巨大网络负载。
- 使用 WebPageTest(WPT)瀑布图来识别第三方阻塞脚本,或使用 WPT 旁路对比来测量第三方标签的影响。
Bundlephobia 等网站有助于评估将可用的 npm 包添加到捆绑包中的成本。您还可以使用 npm 包搜索找到任何包中包含的大小和依赖项。在了解了如何识别问题第三方代码的背景下,让我们探索如何对其进行优化。
优化策略
由于第三方代码不受您的控制,因此您无法直接优化库。这为您提供了两个选择。
替换或移除:如果第三方脚本提供的价值与其性能成本不成比例,请考虑将其移除。您也可以评估其他功能相似但轻量级的替代方案。在此案例研究中,我们讨论了如何通过更换具有类似功能但更轻量的软件包来改善电影应用软件的性能。
优化加载顺序:加载过程涉及在浏览器中加载多个自有资源和第三方资源。为了设计一个最佳的加载策略,您需要考虑浏览器对不同资源的优先级分配、它们在页面上的位置以及每个资源对网页的价值。我们已经为 React/Next.js 应用程序提出了最佳的加载顺序。现在我们将看看这如何适用于各种第三方资源以及我们可以采取哪些步骤来对其进行最佳加载。
- 有效地加载第三方脚本
以下是通过时间验证的最佳实践,可以正确减少第三方资源对性能的影响。 使用异步或延迟加载,防止脚本阻塞其他内容。
适用范围:非关键脚本(标签管理器、分析)
默认情况下,JavaScript 的下载和执行是同步的,可能会阻止主线程上的 HTML 解析器和 DOM 构建。在<script>
元素中使用异步或延迟加载属性告诉浏览器异步下载脚本。您可以使用这些属性来下载任何不属于关键渲染路径的脚本(例如,主要的 UI 组件)。
<script src="https://example.com/deferthis.js" defer></script>
<script src="https://example.com/asyncthis.js" async></script>
2
延迟加载:解析器执行时并行获取脚本,并且脚本执行被延迟到解析完成。对于延迟执行直到 DOM 构建完成的情况,延迟加载应该是默认选择。
异步加载:解析时并行获取脚本,但在其可用时立即执行,从而阻止解析器。对于具有依赖关系的模块脚本,脚本及其所有依赖项将在延迟队列中执行。对于需要在加载过程中较早运行的脚本,请使用异步加载。例如,您可能希望在错过任何早期页面加载数据之前尽早执行特定的分析脚本。
使用资源提示建立与所需来源的早期连接
适用于:来自第三方 CDN 的关键脚本、字体、CSS、图像
<head>
<link
rel="preconnect"
href="http://example.com"
/>
<link
rel="dns-prefetch"
href="http://example.com"
/>
</head>
2
3
4
5
6
7
8
9
10
由于 DNS 查找、重定向和可能针对每个第三方服务器所需的多次往返,因此连接到第三方来源可能会很慢。资源提示 dns-prefetch 和 preconnect 通过在生命周期的早期建立连接,有助于减少这些设置所需的时间。包含与域相对应的 dns-prefetch 资源提示将提前执行 DNS 查找,从而降低与 DNS 查找相关的延迟。您可以将其与最关键资源的 preconnect 配对使用。preconnect 通过与第三方域执行 TCP 往返并处理 TLS 协商来进行连接。关于理想加载顺序的帖子提供了应使用 preconnect 的第三方资源列表。
延迟加载首屏第三方资源
https://www.patterns.dev/vanilla/third-party
自托管 3P 脚本以防止往返
尽可能使用 Service Worker 缓存脚本
遵循理想的加载顺序 (参考上)
关于 JS 加载的最佳实践
有些脚本比其他的更容易优化。与网页性能专家讨论如何优化第三方脚本、观察到的典型限制以及他们对加载第三方脚本的愿望清单,我们得出了一些有趣的结论。普遍的共识是,大多数用户在一定数量的内容可见之前不会与网站进行交互。以下是针对不同脚本类型的指导建议。
- 非关键 JavaScript
大多数第三方如聊天小部件或分析脚本对用户体验来说并不是至关重要的,可以延迟加载。使用 defer 脚本属性是延迟这些脚本的加载和执行的最常见方法。
机器人检测/ReCaptcha
由于您希望阻止机器人访问网页表单,开发者通常会尽早加载这些脚本。然而,ReCaptcha 具有大量的 JS 有效负载和主线程占用空间,因此有动机将其延迟到需要时再加载。优化此脚本的几种方法如下:
仅在包含用户输入的表单页面加载它,这些页面可能会受到机器人的垃圾邮件攻击。 在用户与表单元素进行交互时懒加载脚本,例如,在表单聚焦时。 使用资源提示在需要脚本在页面加载时执行时建立早期连接。
A/B 测试和个性化
A/B 测试
网站利用 A/B 测试来确定哪个网页版本表现更好。在这些测试中,两个页面变体之一会显示给不同用户的样本组。然而,A/B 测试可能会显著影响页面性能,通常会增加最多 1 秒的加载时间。许多此类测试依赖外部第三方脚本,限制了开发者对修改 UI 的 JavaScript 代码的控制。
网站个性化
类似地,网站个性化涉及运行脚本,根据用户数据提供量身定制的体验。这些脚本通常很重且难以优化。像 A/B 测试脚本一样,个性化脚本必须尽早执行,因为渲染的 UI 依赖于它们的输出。
- 优化策略
定制服务器解决方案
开发专门的服务器端解决方案来优化 A/B 测试和个性化是理想的,但并非总是可行。限制用户接触
为了提升第三方 A/B 测试脚本的性能:- 限制接收脚本的用户数量。
- 在脚本中使用启发式方法来识别显示哪个版本,确保只有相关用户受到影响。
利用目标规则
像 Google Optimize 这样的工具允许配置目标规则,使许多评估可以在 Google 服务器上进行。这减少了未被测试用户的性能负担。
通过实施这些策略,可以减轻与 A/B 测试和个性化相关的性能影响,确保所有用户获得更好的体验。
嵌入
- YouTube 和视频地图
这些嵌入内容很重,开发人员必须探索延迟加载或点击加载模式来加载嵌入内容以进行优化。在鼓励使用像 lite-youtube-embed 这样的解决方案的同时,要注意在 iOS/macOS Safari 中需要使用双击/点击才能通过这个界面播放视频。
- 社交媒体嵌入
一些社交媒体嵌入提供了延迟加载脚本的选项(例如 Facebook 嵌入中的 data-lazy)。您可以探索这一点以提高性能。另一种选择是使用手动创建或使用工具如 tweetpik 创建的图像界面。
小结
当您组合网页时,将来自服务器的资源与来自网络其他角落的资源结合起来,您必须经常监视这些资源之间的交互。您可以从正确排序资源和遵循最佳实践开始。您还可以依靠那些在设计时已经内置了这些最佳实践的框架或解决方案。
随着网站的发展,性能报告和定期审计可以帮助消除冗余并优化影响性能的脚本。最后,我们可以希望那些有常见性能问题的第三方会在其端优化代码或公开 API,以使用解决方法解决这些问题。
Tree Shaking
我们可能会向我们的包中添加未在应用程序中使用的代码。为了减小包的大小并防止不必要地加载更多数据,可以消除这些无用的代码!在将代码添加到我们的包之前消除无用的代码的过程称为摇树优化(tree-shaking)。虽然摇树优化对于像数学模块这样的简单模块有效,但在某些情况下,摇树优化可能会很复杂。
例子和具体说明还是看原文吧,翻译的比较晦涩
https://www.patterns.dev/vanilla/tree-shaking
Imports
只有使用 ES2015 模块语法(import 和 export)定义的模块才能进行摇树优化(Tree Shaking)。您导入模块的方式决定了模块是否可以进行摇树优化。摇树优化起始于访问入口文件的所有具有副作用的部分,然后遍历图形边缘直到达到新的部分。遍历完成后,JavaScript bundle 仅包含遍历期间达到的部分。其他部分被排除在外。
副作用
当我们导入一个 ES6 模块时,该模块会立即执行。可能会发生的情况是,尽管我们没有在代码中引用模块的导出内容,模块本身在执行时会影响全局作用域(例如 polyfill 或全局样式表)。这被称为副作用。尽管我们没有引用模块本身的导出内容,但如果模块已经导出了一些值,那么由于导入时的特殊行为,该模块将无法摇树优化!