likes
comments
collection
share

评估脚本和长任务

作者站长头像
站长
· 阅读数 43

加载脚本时,浏览器需要时间在执行之前评估它们,这可能会导致任务很长。了解脚本评估是如何工作的,以及如何避免在页面加载期间产生长时间的任务。

当谈到优化与 Next Paint (INP) 的交互时,您会遇到的大多数建议都是优化交互本身。例如,在优化长任务指南setTimeout中,讨论了 yield with 、isInputPending等技术。这些技术是有益的,因为它们通过避免长任务让主线程有一些喘息的空间,这可以让交互和其他活动更快地运行,而不是等待一个长任务。

但是,加载脚本本身带来的长任务呢?这些任务会干扰用户交互并在加载期间影响页面的INP。本指南将探讨浏览器如何处理脚本评估启动的任务,并研究您可以做些什么来分解脚本评估工作,以便您的主线程可以在页面加载时更好地响应用户输入。

什么是脚本评估?

如果您分析过一个包含大量 JavaScript 的应用程序,您可能已经看到过长的任务,其中"罪魁祸首"被标记为Evaluate Script

评估脚本和长任务

脚本评估工作如 Chrome DevTools 中的性能分析器所示。在这种情况下,该工作足以导致阻塞主线程执行其他工作的长任务——包括驱动用户交互的任务。

脚本评估是在浏览器中执行 JavaScript 的必要部分,因为 JavaScript 是在执行前即时编译的。评估脚本时,首先会对其进行分析以查找错误。如果解析器没有发现错误,脚本就会被编译成字节码,然后继续执行。

尽管有必要,但脚本评估可能会出现问题,因为用户可能会在页面最初呈现后不久尝试与该页面进行交互。然而,仅仅因为页面已经呈现并不意味着页面已经完成加载。加载期间发生的交互可能会延迟,因为页面正忙于评估脚本。不能保证所需的交互可以在此时发生——因为负责它的脚本可能尚未加载——可能存在依赖于JavaScript的交互,或者交互性根本不依赖于JavaScript。

总阻塞时间 (TBT)可以让您深入了解在页面加载期间是否发生了过多的脚本评估,因为它是一种负载响应指标。因为 TBT 与 INP 相关性很好,具有高 TBT 的页面是一个合理的指标,表明在加载期间可能存在与脚本评估工作相关的高 INP 值。

脚本和评估它们的任务之间的关系

负责脚本评估的任务如何启动取决于您正在加载的脚本是通过常规<script>元素加载的,还是脚本是加载了type=module。 由于浏览器倾向于以不同的方式处理事情,因此主要浏览器引擎如何处理脚本评估将涉及到脚本评估行为的不同之处。

使用<script>元素加载脚本

本节介绍使用不带type=module属性的元素加载<script>脚本。

分配给评估脚本的任务数量通常与<script>页面上的元素数量有直接关系。每个<script>元素都会启动一个任务来评估请求的脚本,以便对其进行解析、编译和执行。基于 Chromium 的浏览器、Safari和 Firefox就是这种情况。

为什么这很重要?假设您正在使用捆绑器来管理您的生产脚本,并且您已将其配置为页面运行所需的所有内容捆绑到一个脚本中。如果您的网站是这种情况,您可以预期将分派一个任务来评估该脚本。这是坏事吗?不一定——除非那个脚本很大。

您可以通过避免加载大块 JavaScript 来分解脚本评估工作,并使用其他<script>元素加载更多单独的、更小的脚本。

由于设备的功能各不相同,因此很难为单个脚本的大小定义一个设置限制。要在压缩效率、下载时间和脚本评估时间之间取得适当的平衡,每个脚本 100 KB 的限制是一个很好的目标。

虽然您应该始终努力在页面加载期间加载尽可能少的 JavaScript,但拆分脚本可确保您拥有更多不会阻塞主线程的小任务,而不是一个可能阻塞主线程的大任务线程——或者至少比你开始时少。

评估脚本和长任务

由于页面 HTML 中存在多个<script>元素,产生了多个任务来评估脚本。这比向用户发送一个大脚本包更可取,后者更有可能阻塞主线程。

目前,基于 chrome 的浏览器将在与DOMContentLoaded事件相同的任务中执行所有带有defer属性的加载脚本。虽然这最大限度地减少了总体布局工作,但代价是增加了较长任务的可能性,这可能会导致其他性能问题。目前正在探索一个潜在的解决方案。使用该属性加载的脚本也会发生此行为,因为默认情况下会延迟此类脚本。

使用带有 type=module 属性的 <script> 加载脚本

如果您不捆绑 ES 模块并使用该type=module属性加载它们,您实际上最终可能会减慢页面启动速度。有关更多信息,请阅读本指南后面的权衡和注意事项部分。

现在可以使用script元素上的type=module属性在浏览器中本地加载 ES 模块。这种加载脚本的方法给开发人员带来了一些体验的好处,例如不必为生产用途转换代码——尤其是在与import maps结合使用时。但是,以这种方式加载脚本会安排不同浏览器的任务。

Chrome

在诸如 Chrome 之类的浏览器(或从其派生的浏览器)中,使用type=module属性加载ES模块会产生与不使用type=module时不同的任务类型。例如,将运行每个模块脚本的任务,其中涉及标记为Compile module 的活动。

评估脚本和长任务

基于 Chrome 的浏览器中的模块加载行为。每个模块脚本都会产生一个“编译模块”调用,以在评估之前编译它们的内容。

模块编译完成后,随后在其中运行的任何代码都将启动标记为Evaluate module 的活动。

评估脚本和长任务

这里的效果(至少在Chrome和相关浏览器中),当使用ES模块时,编译步骤被分解。在管理长期任务方面,这是一个明显的胜利;然而,由此产生的模块评估工作仍然意味着您将产生一些不可避免的成本。虽然你应该努力尽可能少地发布JavaScript,但使用ES模块(无论使用哪种浏览器)提供了以下好处:

  • 所有模块代码都在严格模式下自动运行,这允许 JavaScript 引擎进行潜在的优化,而这些优化在非严格的上下文中是无法进行的。
  • 默认情况下使用type=module加载的脚本被视为被延迟。可以在加载的脚本上使用async属性来更改此行为。

Safari 和 Firefox

在 Safari 和 Firefox 中加载模块时,每个模块都会在单独的任务中进行评估。理论上这意味着您可以将仅由静态import语句组成的单个顶级模块加载到其他模块,并且加载的每个模块都会产生单独的网络请求和任务来评估它。

使用import()动态加载脚本

动态import()是加载脚本的另一种方法。与必须位于ES模块顶部的静态导入语句不同,,动态import()调用可以出现在脚本中的任何位置,以按需加载一段 JavaScript。这种技术称为代码拆分

import()在提高 INP 方面,有两个优势:

  1. 延迟加载的模块通过减少当时加载的 JavaScript 数量来减少启动期间的主线程争用。这释放了主线程,因此它可以更好地响应用户交互。
  2. 当进行动态import()调用时,每次调用都会有效地将每个模块的编译和评估分离到它自己的任务中。当然,import()加载非常大模块会启动相当大的脚本评估任务,如果交互与import()调用同时发生,这可能会干扰主线程响应用户输入的能力。因此,加载尽可能少的 JavaScript 仍然非常重要。

动态import()调用在所有主要浏览器引擎中的行为都相似:生成的脚本评估任务将与动态导入的模块数量相同。

在 web worker 中加载脚本

Web workers是一种特殊的 JavaScript 用例。Web worker 在主线程上注册,然后 worker 中的代码在它自己的线程上运行。从某种意义上说,这是非常有益的——虽然注册 web worker 的代码在主线程上运行——但 web worker 中的代码却没有。这减少了主线程拥塞,并有助于保持主线程对用户交互的响应速度更快。

除了减少主线程工作之外,web workers本身还可以加载外部脚本以在 worker 上下文中使用,要么通过importScripts,要么通过支持模块工作器的浏览器中的静态导入语句。结果是,web工作者请求的任何脚本都在主线程之外进行评估。

权衡和注意事项

虽然将您的脚本分解成单独的、较小的文件有助于限制长时间的任务,而不是加载更少、更大的文件,但在决定如何分解脚本时考虑一些因素很重要。

压缩效率

压缩是分解脚本的一个因素。当脚本较小时,压缩效率会有所降低。较大的脚本将从压缩中获益更多。虽然提高压缩效率有助于尽可能缩短脚本的加载时间,但要确保将脚本分解成足够小的块,以在启动期间促进更好的交互性,这需要一点平衡。

捆绑器是管理网站所依赖脚本输出大小的理想工具:

  • 就 webpack 而言,它的SplitChunksPlugin插件可以提供帮助。请参阅SplitChunksPlugin文档以了解可以设置的选项以帮助管理资源大小。
  • 对于Rollupesbuild等其他打包器,您可以通过import()在代码中使用动态调用来管理脚本文件大小。这些打包器——以及 webpack——会自动将动态导入的资源分解到它自己的文件中,从而避免更大的初始打包大小。

缓存失效

缓存失效对页面在重复访问时的加载速度起着重要作用。当您发布大型、单一的脚本包时,您在浏览器缓存方面处于劣势。这是因为当您更新第一方代码时——无论是通过更新包还是发布错误修复——整个包都会失效,必须重新下载。

通过分解脚本,您不仅将脚本评估工作分解为较小的任务,还增加了回访访问者从浏览器缓存而不是从网络获取更多脚本的可能性。这意味着整体页面加载速度更快。

嵌套模块和加载性能

如果您在生产环境中发布 ES 模块并使用type=module属性加载它们,您需要了解模块嵌套如何影响启动时间。模块嵌套是指当一个ES模块静态导入另一个ES模块时,该ES模块静态导入另一个ES模块:

// a.js
import {b} from './b.js';

// b.js
import {c} from './c.js';

如果你的 ES 模块没有捆绑在一起,前面的代码会产生一个网络请求链:当a.js从一个<script>元素请求时,会为b.js发送另一个网络请求,然后涉及对c.js的另一个请求。避免这种情况的一种方法是使用捆绑器——但要确保将捆绑器配置为分解脚本以分散脚本评估工作。

如果你不想使用捆绑器,那么另一种绕过嵌套模块调用的方法是使用modulepreload资源提示,它会提前预加载 ES 模块以避免网络请求链。但是请注意:此提示目前仅在基于 Chrome 的浏览器中受支持。

结论

在浏览器中优化脚本评估无疑是一项棘手的壮举。该方法取决于您网站的要求和限制。但是,通过拆分脚本,您将脚本评估工作分散到许多较小的任务中,因此使主线程能够更有效地处理用户交互,而不是阻塞主线程。

回顾一下,您可以执行以下操作来分解大型脚本评估任务:

  • 使用不带type=module属性加载<script>脚本时,请避免加载非常大的脚本,因为它们会启动阻塞主线程的资源密集型脚本评估任务。将您的脚本分散到更多<script>元素上以分解这项工作。

  • 使用type=module属性在浏览器中本地加载 ES 模块将启动单独的任务以评估每个单独的模块脚本。

  • 通过使用import()动态调用减少初始包的大小。这也适用于捆绑器,因为捆绑器会将每个动态导入的模块视为一个“拆分点”,从而为每个动态导入的模块生成一个单独的脚本。

  • 一定要权衡压缩效率和缓存时效等。较大的脚本会更好地压缩,但更有可能在较少的任务中涉及更昂贵的脚本评估工作,并导致浏览器缓存失效,从而导致整体缓存效率降低。

  • 如果在没有捆绑的情况下原生使用 ES 模块,请使用modulepreload资源提示来优化它们在启动期间的加载。

  • 一如既往,尽可能少地发布 JavaScript。

这当然是一种平衡行为——但通过分解脚本并通过 import() 减少初始负载,您可以获得更好的启动性能,并在关键的启动期间更好地适应用户交互。这应该可以帮助您在 INP 指标上得分更高,从而提供更好的用户体验。

转载自:https://juejin.cn/post/7242235280967041061
评论
请登录