通过代码拆分减少 JavaScript 有效负载

大多数网页和应用程序都由许多不同的部分组成。与其在首次加载页面时发送构成应用程序的所有 JavaScript,不如将 JavaScript 拆分为多个代码块,从而提高页面性能。

本代码实验室演示了如何使用代码拆分来提高一个简单应用程序的性能,该应用程序用于对三个数字进行排序。

A browser window shows an application titled Magic Sorter with three fields for inputting numbers and a sort button.

衡量

与往常一样,重要的是首先衡量网站的性能,然后再尝试添加任何优化。

  1. 要预览网站,请按查看应用。然后按全屏 全屏
  2. 按 `Control+Shift+J`(在 Mac 上按 `Command+Option+J`)打开 DevTools。
  3. 点击 Network 选项卡。
  4. 选中 Disable cache 复选框。
  5. 重新加载应用。

Network panel showing 71.2 KB JavaScript bundle.

仅用于在简单应用程序中对几个数字进行排序,就加载了 71.2 KB 的 JavaScript。这是怎么回事?

在源代码 (src/index.js) 中,导入了 lodash 库并在此应用程序中使用。Lodash 提供了许多有用的实用程序函数,但此处仅使用了该软件包中的一个方法。安装和导入整个第三方依赖项(而仅使用其中的一小部分)是一个常见的错误。

优化

可以通过几种方法来减少捆绑包大小

  1. 编写自定义排序方法,而不是导入第三方库
  2. 使用内置的 Array.prototype.sort() 方法进行数值排序
  3. 仅从 lodash 导入 sortBy 方法,而不是整个库
  4. 仅在用户点击按钮时下载用于排序的代码

选项 1 和 2 是减少捆绑包大小的完全适当的方法(并且对于实际应用程序而言可能是最有意义的)。但是,为了教学目的,本教程中未使用这些方法 😈。

选项 3 和 4 都有助于提高此应用程序的性能。本代码实验室的接下来的几节将介绍这些步骤。与任何编码教程一样,始终尝试自己编写代码,而不是复制和粘贴。

仅导入您需要的内容

需要修改几个文件才能仅从 lodash 导入单个方法。首先,替换 package.json 中的此依赖项

"lodash": "^4.7.0",

替换为此

"lodash.sortby": "^4.7.0",

现在在 src/index.js 中,导入此特定模块

import "./style.css";
import _ from "lodash";
import sortBy from "lodash.sortby";

并更新值的排序方式:

form.addEventListener("submit", e => {
  e.preventDefault();
  const values = [input1.valueAsNumber, input2.valueAsNumber, input3.valueAsNumber];
  const sortedValues = _.sortBy(values);
  const sortedValues = sortBy(values);

  results.innerHTML = `
    <h2>
      ${sortedValues}
    </h2>
  `
});

重新加载应用程序,打开 DevTools,然后再次查看 Network 面板。

Network panel showing 15.2 KB JavaScript bundle.

对于此应用程序,捆绑包大小减少了 4 倍以上,而且工作量很小,但仍有改进空间。

代码拆分

webpack 是当今最流行的开源模块捆绑器之一。简而言之,它将构成 Web 应用程序的所有 JavaScript 模块(以及其他资源)捆绑到浏览器可以读取的静态文件中。

此应用程序中使用的单个捆绑包可以拆分为两个单独的代码块

  • 一个负责构成我们初始路由的代码
  • 第二个代码块,其中包含我们的排序代码

通过使用动态导入,可以延迟加载或按需加载第二个代码块。在此应用程序中,仅当用户按下按钮时才加载构成该代码块的代码。

首先,删除 src/index.js 中排序方法的顶层导入

import sortBy from "lodash.sortby";

并在按下按钮时触发的事件侦听器中导入它

form.addEventListener("submit", e => {
  e.preventDefault();
  import('lodash.sortby')
    .then(module => module.default)
    .then(sortInput())
    .catch(err => { alert(err) });
});

import() 功能是 提案(目前处于 TC39 流程的第 3 阶段)的一部分,旨在包含动态导入模块的功能。webpack 已经包含了对此的支持,并遵循提案中规定的相同语法。

import() 返回一个 Promise,当 Promise 解析时,将提供所选模块,该模块将拆分为单独的代码块。在返回模块后,module.default 用于引用 lodash 提供的默认导出。Promise 与另一个 .then 链接,该 .then 调用 sortInput 方法以对三个输入值进行排序。在 Promise 链的末尾,.catch() 用于处理由于错误而拒绝 Promise 的情况。

最后需要做的是在文件末尾编写 sortInput 方法。这需要是一个返回函数的函数,该函数接受从 lodash.sortBy 导入的方法。然后,嵌套函数可以对三个输入值进行排序并更新 DOM。

const sortInput = () => {
  return (sortBy) => {
    const values = [
      input1.valueAsNumber,
      input2.valueAsNumber,
      input3.valueAsNumber
    ];
    const sortedValues = sortBy(values);

    results.innerHTML = `
      <h2>
        ${sortedValues}
      </h2>
    `
  };
}

监控

最后一次重新加载应用程序,并再次密切关注 Network 面板。仅在应用加载后立即下载一个小的初始捆绑包。

Network panel showing 2.7 KB JavaScript bundle.

在按下按钮对输入数字进行排序后,将获取并执行包含排序代码的代码块。

Network panel showing 2.7 KB JavaScript bundle followed by a 13.9 KB JavaScript bundle.

请注意数字仍然是如何排序的!

结论

代码拆分和延迟加载可能是非常有用的技术,可以缩小应用程序的初始捆绑包大小,这可以直接带来更快的页面加载时间。但是,在将此优化包含在应用程序中之前,需要考虑一些重要事项。

延迟加载 UI

当延迟加载特定的代码模块时,重要的是要考虑网络连接较弱的用户体验。当用户提交操作时,拆分和加载非常大的代码块可能会使应用程序看起来好像已停止工作,因此请考虑显示某种加载指示器。

延迟加载第三方节点模块

在应用程序中延迟加载第三方依赖项并不总是最佳方法,这取决于您在何处使用它们。通常,第三方依赖项被拆分为单独的 vendor 捆绑包,该捆绑包可以被缓存,因为它们不会经常更新。阅读有关 SplitChunksPlugin 如何帮助您执行此操作的更多信息。

使用 JavaScript 框架进行延迟加载

许多使用 webpack 的流行框架和库都提供了抽象,使延迟加载比在应用程序中间使用动态导入更容易。

尽管了解动态导入的工作原理很有用,但始终使用您的框架/库推荐的方法来延迟加载特定模块。

预加载和预取

在可能的情况下,利用浏览器提示,例如 <link rel="preload"><link rel="prefetch">,以尝试更快地加载关键模块。webpack 通过在导入语句中使用魔术注释来支持这两种提示。这在预加载关键代码块指南中进行了更详细的解释。

延迟加载代码以外的内容

图像可能构成应用程序的重要组成部分。延迟加载折叠下方或设备视口外部的图像可以加快网站速度。在Lazysizes 指南中阅读有关此内容的更多信息。