优化加载第三方

第三方资源可能会降低网站速度,并且可能是优化的挑战。

可以遵循某些最佳实践来有效地加载或延迟不同类型的第三方。

还可以使用框架级组件,例如 Next.js 脚本组件,它提供了一个模板,用于构建加载第三方脚本的“何时”和“如何”。

或者,像 Partytown 这样的实验性想法可能会引起人们的兴趣。

很难找到一个在孤岛中运作的现代网站。

大多数站点共存并依赖网络上的其他几个来源来获取数据、功能、内容等。

位于另一个域并由您的站点使用的任何资源都是您站点的第三方 (3P) 资源。

站点中包含的典型第三方资源包括

  • 嵌入地图、视频、社交媒体和聊天服务

  • 广告

  • 分析组件和标签管理器

  • A/B 测试和个性化脚本

  • 提供即用型辅助功能的实用程序库,例如用于数据可视化或动画的功能。

  • reCAPTCHA 或 CAPTCHA 用于机器人检测。

可以使用第三方来集成其他功能,
为内容增加价值或减少从头开始构建网站所涉及的一些苦差事。

根据 2021 年网络年鉴报告,网络上超过 94% 的页面使用第三方——图像和 JavaScript 是第三方内容的最重要贡献者。

以下是按内容类型和类别对第三方请求进行的有用细分:

虽然第三方资源可以通过有价值的功能丰富您的网站,但如果出现以下情况,它们也会减慢速度:

  • 对于每个所需的资源,它们都会导致到第三方域的额外往返。

  • 它们大量使用 JavaScript(影响下载和执行时间)或由于未优化的图像/视频而体积庞大。

  • 个别网站所有者无法影响实施,他们的行为可能无法预测。

  • 它们可以阻止页面上其他关键资源的呈现并影响 Core Web Vitals (CWV)。

尽管存在这些问题,第三方可能对业务至关重要。

如果不能取消 3P,那么接下来最好的事情就是优化它们以减少性能影响 - 这就是本节中介绍的内容。

包含了一些适用于不同类型第三方脚本的策略和最佳实践。

Next.js Script 组件包含许多这些最佳实践,可以在本文的后半部分了解它。

首先看看如何确定第三方脚本是否会损害页面的性能。

评估 3P 资源的性能影响

可以使用多种技术来查找第三方代码如何影响网站。

以下 Lighthouse 审计有助于识别影响 CWV 的缓慢第三方脚本。

  • 减少第三方代码对阻塞主线程的脚本的影响。

  • 减少需要很长时间执行的脚本的 JavaScript 执行时间

  • 避免大型脚本的巨大网络负载



  • 使用 WebPageTest (WPT) 瀑布图来识别第三方阻止脚本或 WPT 并排比较来衡量 3rd 方标签的影响。

  • Bundlephobia 等网站有助于评估将可用 npm 包添加到您的包中的成本。
    还可以使用 npm 包搜索查找任何包中包含的大小和依赖项。

有了识别有问题的第三方代码的背景,让我们探索优化它的方法。


优化策略

由于第三方代码不受您的控制,因此您无法直接优化库。 这给您留下了两个选择。

  1. 替换或移除:
    如果第三方脚本提供的值与其性能成本不成正比,请考虑将其移除。
    还可以评估其他轻量级但提供类似功能的替代方案。
    在本案例研究中,将讨论如何通过切换具有更轻量级替代品和类似功能的软件包来提高电影应用程序的性能。

  2. 优化加载顺序:
    加载过程涉及在浏览器中加载多个第一方和第三方资源。
    要设计最佳加载策略,需要考虑为不同资源分配的浏览器优先级、它们在页面上的位置以及每个资源在网页中的价值。
    我们为 React/Next.js 应用程序提出了最佳加载顺序。 我们现在将看到这如何适用于各种第三方资源以及可以采取哪些步骤以最佳方式加载它们。

高效加载 3P 脚本

以下是经过时间考验的最佳实践,如果使用得当,它们可以减少第三方资源的性能影响。

使用 asyncdefer 来防止脚本阻止其他内容。
适用于:非关键脚本(标签管理器、分析)

JavaScript 下载和执行默认是同步的,可以阻止主线程上的 HTML 解析器和 DOM 构建。
<script> 元素中使用 asyncdefer 属性告诉浏览器异步下载脚本。
可以使用这些来下载关键渲染路径(例如,主 UI 组件)不需要的任何脚本

defer:脚本在解析器执行时并行获取,脚本执行延迟到解析完成。
Defer 应该是延迟执行直到 DOM 构建之后的默认选择。

async:脚本在解析时并行获取,但在阻塞解析器时会在可用时立即执行。
对于有依赖的模块脚本,脚本及其所有依赖都在 defer 队列中执行。
对需要在加载过程中更早运行的脚本使用 async。
例如,可能希望在不丢失任何早期页面加载数据的情况下尽早执行特定的分析脚本。

<script src="https://example.com/deferthis.js" defer></script>
<script src="https://example.com/asyncthis.js" async></script>

developers.google.com

这里值得一提的一个警告是 asyncdefer 降低了浏览器分配的资源优先级,导致其加载时间显着延迟。
优先级提示的新功能可以帮助解决此问题。

使用资源提示建立与所需来源的早期连接

适用于:来自第三方 CDN 的关键脚本、字体、CSS、图像

由于每个第三方服务器可能需要 DNS 查找、重定向和多次往返,连接到第三方源可能会很慢。

资源提示 dns-prefetchpreconnect 通过在生命周期的早期启动连接来帮助减少此设置所需的时间。

包含与域对应的 dns-prefetch 资源提示将提前执行 DNS 查找,从而减少与 dns 查找相关的延迟。 可以将其与最关键资源的预连接配对。

除了 DNS 查找之外,预连接还通过执行 TCP 往返和处理 TLS 协商来启动与第三方域的连接。

<head>
    <link rel="preconnect" href="http://example.com">
    <link rel="dns-prefetch" href="http://example.com">
</head>

延迟加载 3P 资源

适用于:YouTube、地图、广告和社交媒体等嵌入

第三方嵌入(例如用于社交媒体提要、广告、YouTube 视频和地图的嵌入)可能会降低网页速度。

但是,所有此类嵌入在页面加载时可能对用户不可见,并且在用户向下滚动到它们时可能会延迟加载。

可以根据所需的浏览器支持使用不同的延迟加载方法。

  1. loading 属性可以与图像和 iframe 一起使用,这些图像和 iframe 通常用于加载第三方嵌入,例如 YouTube 或 Google 地图的嵌入。

  2. 使用 IntersectionObserver API 的自定义检测被观察元素何时进入或退出浏览器的视口。

  3. Lazy-sizes - 一个流行的 JavaScript 库,可为实现延迟加载。

延迟加载嵌入的一种变体使用在页面加载时向用户显示的静态或动态外观。

可以使用实际嵌入的静态图像代替地图嵌入来显示地图嵌入上的特定区域。

或者,使用看起来像嵌入但仅在用户单击或与之交互时加载的外观。

流行的嵌入实现外观的一些方法包括用于地图的 Map Static API、用于 Twitter 嵌入的 Tweetpik、用于 YouTube 的 lite-youtube-embed、用于聊天小部件的 React-live-chat-loader

有关这些技术的全面讨论可在此处获得。

关于延迟加载和外墙的一些注意事项

  • YouTube 外观在 iOS 和 Safari 上的 macOS 11+ 上的行为略有不同。 第一次点击/点击加载实际的视频嵌入。 用户必须再次点击才能播放视频。

  • 如果未指定嵌入的大小,延迟加载会导致布局偏移并影响用户体验。 为了防止布局偏移,应该为所有延迟加载的嵌入或其容器元素指定大小。

自托管 3P 脚本以防止往返

适用于:JavaScript 文件、字体

  • 尽管 preconnectdns-prefetch 允许尽早启动与第三方源的连接,但这些连接仍然是必需的。此外,对于第三方来源,需要依赖他们的缓存策略,这可能不是最佳的。

  • 在同一源上自托管脚本的副本可更好地控制用于脚本的加载和缓存过程。自托管减少了 DNS 查找所需的时间,并允许您使用 HTTP 缓存改进脚本的缓存策略。还可以使用 HTTP/2 服务器推送来推送您知道用户需要的脚本。自托管第三方脚本的一个很好的例子是 Casper.com,它通过 Optimizely 提供的自托管第三方脚本将其主页的开始渲染时间缩短了 1.7 秒。

对于第三方脚本的自托管副本,必须确保根据对原始脚本的更改定期更新副本。如果没有更新,脚本可能会过时,缺少与依赖项对应的重要修复或更改。在服务器上自托管而不是 CDN 也会阻止您利用 CDN 采用的边缘缓存机制。

尽可能使用 Service Worker 缓存脚本

适用于:JavaScript 文件、字体

对于频繁更改的脚本,自托管可能不是一个选项。
可以使用 Service Worker 来改进此类第三方脚本的缓存,同时还可以利用 CDN 边缘缓存。
这种技术可以更好地控制通过网络重新获取的频率。可以与预连接结合使用,以进一步降低获取操作的网络成本。
还可以加载资源,以便将非必要的第三方脚本的请求推迟到页面到达关键用户时刻。

遵循理想的加载顺序
考虑上述针对不同类型第三方的指南及其对页面的价值。 根据每个资源的预期用途,可以遵循理想的资源加载顺序,以最佳方式交错第一方和第三方资源,以加快页面加载速度。


按脚本类型划分的最佳实践

有些脚本比其他脚本更容易优化。

与 Web 性能专家就优化不同的第三方、观察到的一些典型约束以及他们加载第三方的愿望清单进行讨论后,得出了一些有趣的结论。

普遍的共识是,大多数用户在看到某个特定阈值的内容之前不会与站点进行交互。

以下是特定于不同脚本类型的指南。

非关键 JavaScript

大多数第三方(如聊天小部件或分析脚本)对用户体验并不重要,可能会延迟。
使用 defer 脚本属性是延迟这些脚本的加载和执行的最常用方法。

广告或分析团队可能会担心推迟脚本对应用程序的可见性和广告收入的影响。
Telegraph 案例研究经常在这种情况下被引用,其中推迟所有脚本不会影响任何分析或广告指标。
相反,第一个广告加载指标平均提高了 4 秒。
一些开发人员还设计了一些解决方案,将第三方的加载延迟到页面变为可交互之后

机器人检测/ReCaptcha

由于希望阻止机器人访问 Web 表单,因此开发人员通常会尽早加载这些脚本。

然而,ReCaptcha 有相当大的 JS 负载和主线程占用空间,因此有动力将它推迟到需要时加载。

优化此脚本的几种方法是

  1. 仅在几个页面上加载它,其中包含可能被机器人发送垃圾邮件的用户表单输入。

  2. 当用户与表单交互时延迟加载脚本,例如,在表单焦点上。

  3. 当需要在页面加载时执行脚本,使用资源提示建立早期连接。

谷歌标签管理器 (GTM)

大型网站通常允许 Google 跟踪代码管理器访问营销团队或代理机构。

这允许他们向网站上的所有页面添加新的营销标签,以便更好地进行跟踪。

性能不是营销团队的主要关注点,他们所有人可能都不知道,不加考虑地添加标签会减慢网站的速度。

GTM 脚本的优化更多是关于控制谁访问 GTM 并监控他们所做的更改。

可以首先确保网站所有者拥有帐户而不是外部机构。这允许您为可以添加、编辑和发布标签的人员定义精细的访问权限。

可以在开发和营销部门之间建立更好的协作来审核新标签并删除未使用的标签。

网站可能不需要在所有页面上使用 GTM。
(例如,营销团队没有理由在电子商务网站的结帐页面上跟踪事件)。

应单独审核页面,以便删除不必要的 GTM 内容。

如果用户拒绝 cookie,使用 cookie 横幅的站点也可以选择不加载 GTM。

最后,如果必须在页面上加载 GTM,可以在加载主要内容后推迟触发脚本。

另一个适用于旧第三方脚本标签的优化与 document.write() 相关。
使用 document.write() 注入脚本是不安全的,并且可能会导致基于浏览器和脚本类型的警告或错误。
一些第三方脚本仍然使用这种方法。 GTM 在其自定义 HTML 标签创建界面中提供了一个名为 Support document.write() 的配置。
如果启用此功能,Google 标签管理器会临时用自己的安全版本替换默认的 document.write() 函数。

A/B 测试和个性化

网站进行 A/B 测试以检查哪个版本的网页性能更好。
页面的两种变体之一是为已识别的用户样本中的不同用户加载的。
A/B 测试会显着影响运行它们的页面的性能,每个测试都会增加多达 1 秒的加载时间。
目前,许多 A/B 测试都是通过第三方从外部获取的,开发人员几乎无法控制为更改这些测试的 UI 而执行的 JavaScript 代码。

站点个性化是一个相关概念,它涉及运行脚本以根据已知数据为不同用户提供量身定制的体验。
这些脚本同样很重并且难以优化。与 A/B 测试脚本一样,个性化脚本也需要尽早运行,因为呈现的 UI 取决于脚本的输出。
为 A/B 测试和个性化开发基于服务器的自定义解决方案是优化 A/B 测试的理想方法。然而,它可能并不总是可行的。

为了优化第三方 A/B 测试脚本,您可以限制接收脚本的用户数
该脚本根据试探法确定要显示的版本,并为用户启用正确的版本。这可能会减慢所有用户的页面速度。
Google 的优化器允许配置用于定位用户的规则。其中许多规则都可以在 Google 服务器上进行评估,因此对非目标用户的性能影响很小

YouTube 和地图嵌入

这些嵌入很重,开发人员必须探索延迟加载或点击加载模式来加载嵌入以优化它们。

鼓励使用 lite-youtube-embed 等解决方案,同时注意在 iOS/macOS-Safari 中需要双击/单击才能使用此外观播放视频。

社交媒体嵌入

一些社交媒体嵌入提供了延迟加载脚本的选项(例如,Facebook 嵌入中的数据延迟)。

可以探索它以提高性能。

另一种选择是使用手动创建的图像外观或使用诸如 tweetpik 之类的工具。

开箱即用的优化

为了优化第三方,开发团队应该了解资源提示、延迟加载、HTTP 缓存和服务工作者的细微差别,然后在他们的解决方案中实现这些。一些框架和库以开发人员可以轻松使用的方式封装了这些最佳实践。

由 Builder.io 创建的 Partytown 是一个实验性库,可帮助在 Web Worker 而不是主线程上运行资源密集型脚本。他们的理念是主线程应该专用于您的代码,关键路径不需要的任何脚本都可以被沙箱化并隔离到网络工作者。 Partytown 允许您配置对主线程 API(如 cookie、localStorage、userAgent 等)的访问。API 调用也可以使用参数记录,以便更好地了解脚本的作用。

JavaScript 代理和 Service Worker 处理 Web Worker 和主线程之间的通信。 Partytown 脚本必须自托管在与 HTML 文档相同的服务器上。它可以与 React 或 Next.js 应用程序一起使用,甚至可以在没有任何框架的情况下使用。可以在 Web 服务器中执行的每个第三方脚本都应将其打开脚本标记的类型属性设置为 text/partytown,如下所示。

1
2
3
4

<script type="text/partytown">
    // Third-party analytics scripts
</script>

该库还提供了一个 React Partytown 组件,您可以直接将其包含在您的 React 或 Next.js 项目中。

对于 Next.js 文档,它可以包含在文档 <head> 中,如下所示。

import { Partytown } from '@builder.io/partytown/react';
import Document, { Html, Head, Main, NextScript } from 'next/document';

export default class MyDocument extends Document {
 render() {
   return (
     <Html>
       <Head>
         <Partytown />
       </Head>
       <body>
         <Main />
         <NextScript />
       </body>
     </Html>
   );
 }

Partytown 还包括用于常见分析库(例如 Google 标签管理器)的 React 组件。

以下示例展示了如何将其添加到 React/Next.js 项目中。

import { Partytown, GoogleTagManager, GoogleTagManagerNoScript } from '@builder.io/partytown/react';
import Document, { Html, Head, Main, NextScript } from 'next/document';

export default class MyDocument extends Document {
 render() {
   return (
     <Html>
       <Head>
         <GoogleTagManager containerId={'GTM-XXXXX'} />
         <Partytown />
       </Head>
       <body>
         <GoogleTagManagerNoScript containerId={'GTM-XXXXX'} />
         <Main />
         <NextScript />
       </body>
     </Html>
   );
 }

Next.js 本身通过其 Script 组件为第三方脚本提供开箱即用的优化。

让我们看看这如何让我们提高不同第三方的加载性能。


Next.js 脚本组件 ✔

Next.js 11 于 2021 年年中发布,其组件基于 Google 的 Aurora 团队引入的一致性方法。

Conformance 是一个系统,它提供精心设计的解决方案和规则,以支持最佳加载和 Core Web Vitals。

一致性将最佳实践编入规则集,开发人员可以轻松实施。

强大的默认设置和可操作的规则构成了该系统的基础。

它们使开发人员可以轻松地做正确的事情并防止反模式潜入。

Next.js 脚本组件通过提供可提高加载性能的可自定义模板来使用一致性。

Script 组件封装了 <script> 标签,允许使用 strategy 属性设置第三方脚本的加载优先级。

策略属性可以采用三个值。

  1. beforeInteractive:将此用于浏览器应在页面变为交互式之前执行的关键脚本。 (例如,机器人检测)

  2. afterInteractive:用于在页面交互后浏览器可以运行的脚本。 (例如,标签管理器)这是应用的默认策略,相当于使用 defer 加载脚本

  3. lazyOnload:用于在浏览器空闲时可以延迟加载的脚本。

设置策略有助于 Next.js 自动应用优化和最佳实践来加载脚本,同时确保最佳加载顺序。

可以使用带有策略属性的脚本标记,如下所示。

与原生 HTML 脚本标签不同,不得将 next/script 标签放置在 next/head 组件或 pages/document.js 中。

Before:

import Head from 'next/head'

export default function Home() {
 return (
   <>
     <Head>
       <script async src="https://example.com/samplescript.js" />
     </Head>
   </>
 )
}

After:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

// pages/index.js
// 当未指定策略时,将应用默认策略 afterinteractive。
import Script from 'next/script'
<br>
export default function Home() {
 return (
   <>
     <Script src="https://example.com/samplescript.js" />
   </>
 )
}

Script 组件允许处理前面讨论的许多用例。

可以使用它来加载用于分析、社交媒体、实用程序库等的第三方脚本。

以下示例演示了如何将上述策略应用于不同类型的第三方脚本。

尽早加载 polyfill

如果希望提前加载适用于核心内容的特定 polyfill,可以使用 beforeInteractive 策略加载 polyfill,

如 Next.js 文档中的以下示例所示。

import Script from 'next/script'

export default function Home() {
 return (
   <>
     <Script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserverEntry%2CIntersectionObserver"
       strategy="beforeInteractive"
     />
   </>
 )
}

延迟加载社交媒体嵌入

社交媒体嵌入,尤其是那些在页面加载时不可见的嵌入,
可以在用户滚动到它们时或在不活动期间延迟或延迟加载。

可以使用 lazyonload 策略,如以下代码段所示。

import Script from 'next/script'

export default function Home() {
 return (
   <>
     <Script
       src="https://connect.facebook.net/en_US/sdk.js" strategy="lazyOnload"
     />
   </>
 )
}

在加载时有条件地执行代码

可能有一些代码需要在特定的第三方加载后执行。
这可以在脚本组件的 onload 属性中指定。

例如,以下代码段显示了如何包含将根据用户同意执行的代码。

1
2
3
4
5
6
7
8

<Script
   src={url} // 同意管理
   strategy="beforeInteractive"
   onLoad={() => {
     // 如果加载成功,则可以依次加载其他脚本
   }}
/>

在脚本标签中使用内联脚本

需要根据第三方组件的负载执行的内联脚本也可能包含在 Script 组件中,如下所示。

import Script from 'next/script'

<Script id="show-banner" strategy="lazyOnload">
 {`document.getElementById('banner').removeClass('hidden')`}
</Script>

// or

<Script
 id="show-banner"
 dangerouslySetInnerHTML={{
   __html: `document.getElementById('banner').removeClass('hidden')`
 }}
/>

这里的内联脚本用于在延迟加载后更改第三方横幅广告的可见性。
请注意,也可以使用危险的 SetInnerHTML 属性包含内联脚本。

将属性转发给第三方脚本

可以在脚本组件中设置第三方脚本可以使用的特定属性值。

以下示例显示了如何将两个这样的属性传递给分析脚本。

import Script from 'next/script'

export default function Home() {
 return (
   <>
     <Script
       src="https://www.google-analytics.com/analytics.js"
       id="analytics"
       nonce="XUENAJFW"
       data-test="analytics"
     />
   </>
 )
}

加载分析脚本

使用 Google Analytics (GA) 和 Google Tag Manager (GTM) 可以通过不同的方式在网站上包含分析。

可以使用脚本组件在 Next.js 网站上以最佳方式加载 gtag.jsanalytics.js 脚本。 根据您要执行这些脚本的位置,您可以在 _app.js(适用于所有页面)或特定页面上加载它们。

通过在 _app.js 中包含脚本组件,可以为站点上的所有页面启用 GTM,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

import Script from 'next/script'
// + other imports

function MyApp({ Component, pageProps }) {
 // Other app code

 return (
   <>
     {/* Google Tag Manager - Global base code */}
     <Script
       strategy="afterInteractive"
       dangerouslySetInnerHTML={{
         __html: `
           (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
           new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
           j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
           'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
           })(window,document,'script','dataLayer', '${GTM_ID}');
         `,
       }}
     />
     <Component {...pageProps} />
   </>
 )
}
export default MyApp

相反,如果想在特定页面上加载 analytics.js,
可以将其包含在页面中,如图所示。

import Script from 'next/script'
//other imports

const Home = () => {
 return (
   <div class="container">
     <Script id="google-analytics" strategy="afterInteractive">
       {`
         (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
         (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
         m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
         })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');

         ga('create', 'UA-XXXXX-Y', 'auto');
         ga('send', 'pageview');
       `}
     </Script>
   </div>
   //Other UI related HTML
 )
}
import Script from 'next/script'
//other imports

const Home = () => {
 return (
   <div class="container">
     <Script id="google-analytics" strategy="afterInteractive">
       {`
         (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
         (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
         m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
         })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');

         ga('create', 'UA-XXXXX-Y', 'auto');
         ga('send', 'pageview');
       `}
     </Script>
   </div>
   //Other UI related HTML
 )
}

export default Home

请注意,在上面的两个示例中,分析脚本加载了 strategy = afterInteractive


结论

在将来自服务器的资源与来自 Web 其他角落的资源组合起来编写网页时,必须经常监视这些资源之间的相互作用。

可以从正确排序资源并遵循最佳实践开始。 还可以依赖在设计中内置了这些最佳实践的框架或解决方案。

随着站点的发展,性能报告和定期审计可以帮助消除冗余并优化影响性能的脚本。

最后,我们总是希望有众所周知的性能问题的第三方会在他们的最后优化代码或公开 API 以启用变通方法来解决这些问题。


知识点

  • Next.s

  • Partytown