代码拆分 JavaScript

加载大型 JavaScript 资源会显著影响网页速度。拆分 将 JavaScript 拆分成较小的块,并且只下载 确保网页在启动期间能够正常运行,可以显著提高网页的加载速度。 响应速度,进而改善网页的与下一个互动之间的互动 绘制 (INP)

在网页下载、解析和编译大型 JavaScript 文件的过程中, 在一段时间内没有响应。页面元素是可见的, 网页初始 HTML 的一部分,并由 CSS 设置样式。不过,由于 JavaScript 以及为这些互动元素提供支持的其他脚本, 可能是解析和执行 JavaScript 以使它们正常运行。通过 这样用户可能会感觉这次互动 出现延迟甚至完全损坏的情况

这通常是因为在解析 JavaScript 时主线程处于阻塞状态 并在主线程中进行编译如果这个过程耗时过长,而且需要互动 网页元素对用户输入的响应速度可能不够快。一种解决方法 只加载网页正常运行所需的 JavaScript,而 通过一种称为“代码”的技术,将其他 JavaScript 延迟到稍后加载 拆分。本单元着重介绍这两种方法中的后一种。

通过代码拆分减少启动期间的 JavaScript 解析和执行

JavaScript 执行时间超过 2 秒时,Lighthouse 抛出警告 并在超过 3.5 秒时失败。JavaScript 过多 解析和执行在网页中任意点都有潜在问题 因为这可能会增加互动的输入延迟 如果用户与网页互动的时间正好符合 负责处理和执行 JavaScript 的主线程任务 。

除此之外,过多的 JavaScript 执行和解析尤其 在初始网页加载过程中就会出现问题,因为这是网页中 用户很可能会与页面进行交互的生命周期。事实上 总阻塞时间 (TBT) 是一项负载响应指标,高度相关INP 搭配使用,这表明用户尝试互动的意愿很高 所有事件

报告执行每个 JavaScript 文件所用时间的 Lighthouse 审查 网页请求非常有用,它可以帮助您准确识别 脚本可能适合进行代码拆分。然后,您可以 使用 Chrome 开发者工具中的覆盖率工具来准确识别 网页的 JavaScript 在网页加载过程中未被使用。

代码拆分是一项实用的技术,可以减少网页的初始 JavaScript 负载它支持您将 JavaScript 软件包拆分为两部分:

  • 网页加载时所需的 JavaScript,因此无法在其他任何位置加载 。
  • 可稍后加载的其他 JavaScript,最常见的是 用户与指定的互动元素互动时 页面。

您可以使用动态 import() 语法完成代码拆分。这个 语法 - 与请求给定 JavaScript 资源的 <script> 元素不同 在启动期间请求 JavaScript 资源, 页面生命周期

<ph type="x-smartling-placeholder">
document.querySelectorAll('#myForm input').addEventListener('blur', async () => {
  // Get the form validation named export from the module through destructuring:
  const { validateForm } = await import('/validate-form.mjs');

  // Validate the form:
  validateForm();
}, { once: true });

在前面的 JavaScript 代码段中,validate-form.mjs 模块是 仅当用户对表单的任何表单内容模糊处理时,才会下载、解析和执行 <input> 字段。在这种情况下,负责 驱动表单的验证逻辑只涉及页面 最有可能被实际使用。

webpackParcelRollupesbuild 等 JavaScript 捆绑器 将 JavaScript 软件包拆分成更小的区块 在源代码中遇到动态 import() 调用。大部分这些工具 但 esbuild 特别要求您选择启用 优化。

<ph type="x-smartling-placeholder">

代码拆分实用说明

代码拆分是减少主线程争用的有效方法 因此如果您要决定在网页首次加载时 来审核 JavaScript 源代码,看看是否存在代码拆分机会。

使用捆绑器(如果可以)

开发者在测试期间使用 JavaScript 模块的一项常见做法是 开发过程。这是一次极佳的开发者体验改进 提高代码的可读性和可维护性。不过,也有一些 交付 JavaScript 时可能会导致性能欠佳的特征 部署到生产环境。

最重要的是,您应该使用捆绑器处理和优化源代码 包括您想要进行代码拆分的模块。Bundler 在 不仅要对 JavaScript 源代码进行优化,而且还会对 可有效平衡软件包大小等性能因素 压缩比压缩效率随软件包大小而增加, 但打包器也会尽量确保 bundle 不会过大,以免造成 因为脚本评估导致的耗时较长的任务。

Bundler 还可以避免交付大量未捆绑模块的问题 通过网络。使用 JavaScript 模块的架构往往 复杂模块树。将模块树解绑后,每个模块都代表一个 发送一个单独的 HTTP 请求,并且 Web 应用中的交互操作可能会延迟, 不捆绑模块。虽然您可以使用 <link rel="modulepreload"> 资源提示:尽早加载大型模块树 但考虑到加载性能, 标准。

不要无意中停用流式编译

Chromium 的 V8 JavaScript 引擎提供了许多开箱即用的优化功能 以确保尽可能高效地加载生产 JavaScript 代码。 其中一项优化称为流式编译,例如 对流式传输到浏览器的 HTML 进行增量解析—编译流式数据块 从网络到达的 JavaScript。

您可以通过多种方式确保对代码进行流式编译, Chromium 中的 Web 应用:

  • 转换正式版代码,避免使用 JavaScript 模块。捆绑器 可以根据编译目标转换 JavaScript 源代码,以及 目标通常特定于给定环境。V8 将采用流式传输 编译到任何不使用模块的 JavaScript 代码,并且您可以 配置捆绑器,以将 JavaScript 模块代码转换为语法 不使用 JavaScript 模块及其功能。
  • 如果您希望将 JavaScript 模块发布到生产环境中,请使用 .mjs无论您的正式版 JavaScript 是否使用模块, 对于使用模块(而非 JavaScript)的 JavaScript,没有特殊的内容类型 则不能。而对于 V8 而言,您实际上是停用流式传输, 使用 .js 在生产环境中发布 JavaScript 模块时进行编译 。如果您为 JavaScript 模块使用 .mjs 扩展程序,则 V8 可以 确保不会对基于模块的 JavaScript 代码进行流式编译 损坏。

切勿因这些因素而放弃使用代码拆分。代码 拆分可有效减少用户的初始 JavaScript 载荷, 但可以使用打包器并了解如何保留 V8 的流式传输 编译行为,您可以确保生产 JavaScript 代码与 尽可能为用户提供快速的体验

动态导入演示

Webpack

webpack 随附一个名为 SplitChunksPlugin 的插件,可让你 配置捆绑器拆分 JavaScript 文件的方式。webpack 识别 动态 import() 和静态 import 语句。行为 SplitChunksPlugin 可以通过指定 chunks 选项在其 配置:

  • chunks: async 是默认值,表示动态 import() 调用。
  • chunks: initial 是指静态 import 调用。
  • chunks: all 同时涵盖了动态 import() 和静态导入,让您能够 用于在 asyncinitial 导入之间共享分块。

默认情况下,每当 webpack 遇到动态 import() 语句时。它 会为该模块创建一个单独的分块:

/* main.js */

// An application-specific chunk required during the initial page load:
import myFunction from './my-function.js';

myFunction('Hello world!');

// If a specific condition is met, a separate chunk is downloaded on demand,
// rather than being bundled with the initial chunk:
if (condition) {
  // Assumes top-level await is available. More info:
  // https://v8.dev/features/top-level-await
  await import('/form-validation.js');
}

上述代码段的默认 webpack 配置会导致出现两个 拆分数据块:

  • main.js 分块(Webpack 将其归类为 initial 分块), 包含 main.js./my-function.js 模块。
  • async 分块,仅包含 form-validation.js(包含一个 资源名称中的文件哈希(如果已配置)。此分块仅会下载 如果 conditiontruthy

采用此配置时,您可以将 form-validation.js 分块延迟到 还是需要它的这样就可以减少脚本 评估时间。脚本下载和评估 在满足指定条件时都会发生 form-validation.js 分块,即在 在这种情况下,系统会下载动态导入的模块。例如 条件是只为特定浏览器下载 polyfill,或者如 前面的示例 - 导入的模块是用户互动所必需的。

另一方面,更改 SplitChunksPlugin 配置以指定 chunks: initial 可确保仅在初始分块上拆分代码。这些是 数据块,例如静态导入的块,或 webpack 的 entry 中列出的数据块 属性。看一下前面的示例,生成的分块将是 form-validation.js main.js 组合到单个脚本文件中, 这可能导致初始网页加载性能降低。

SplitChunksPlugin的选项也可以配置为 脚本转换为多个较小的脚本,例如使用 maxSize 选项 指示 Webpack 在数据块超出 由 maxSize 指定。将大型脚本文件拆分为多个较小的文件 提高负载响应能力,例如在某些情况下, CPU 密集型脚本评估 工作分成数个小任务,这些小任务不太可能阻碍主要 延长时间

此外,生成更大的 JavaScript 文件也意味着 更容易遭受缓存失效。例如,如果您提供 包含框架和第一方应用代码的大型脚本, 只更新框架而不更新 捆绑的资源

另一方面,脚本文件越小,返回 从缓存中检索资源,从而可以提高网页在 重复访问。但是,与较大文件相比,较小的文件从压缩中受益较少 并且可能会增加网页加载时的网络往返时间 浏览器缓存。必须注意在缓存之间取得平衡 压缩有效性及脚本评估时间。

<ph type="x-smartling-placeholder">

webpack 演示

<ph type="x-smartling-placeholder">

webpack SplitChunksPlugin 演示

知识测验

执行代码时使用哪种类型的 import 语句 拆分?

动态 import()
正确!
静态 import
请重试。

哪种类型的 import 语句必须位于顶部 而又不在其他位置?

动态 import()
请重试。
静态 import
正确!

在 webpack 中使用 SplitChunksPlugin 时, async 数据块与 initial 块吗?

系统会使用动态 import() 加载 async 个分块 和initial区块使用静态数据块进行加载 import
正确!
使用静态 import 加载 async 分块 和initial分块是使用动态方法加载的 import()
请重试。

下一篇:延迟加载图片和 <iframe> 元素

虽然它往往是一种相当昂贵的资源类型,但 JavaScript 并不是 是可以延迟加载的唯一资源类型图片和 <iframe> 元素 本身可能就是昂贵的资源。与 JavaScript 类似 可以通过延迟加载来延迟加载图片和 <iframe> 元素 它们,我们将在本课程的下一个单元中对此进行介绍。