通过代码拆分减少 JavaScript 载荷

Houssein Djirdeh
Houssein Djirdeh

大多数网页和应用都由许多不同的部分组成。在加载第一个网页时,不要立即发送构成应用的所有 JavaScript,而是将 JavaScript 拆分为多个分块,以提升网页性能。

此 Codelab 介绍了如何使用代码拆分来提升对三个数字进行排序的简单应用的性能。

浏览器窗口显示一个名为“Magic Sorter”的应用,其中包含三个用于输入数字的字段和一个排序按钮。

测量

与往常一样,在尝试添加任何优化之前,请务必先衡量网站的效果。

  1. 如需预览网站,请按查看应用。然后按全屏 全屏。
  2. 按 `Control+Shift+J`(在 Mac 上,按 `Command+Option+J`)打开开发者工具。
  3. 点击网络标签页。
  4. 选中停用缓存复选框。
  5. 重新加载应用。

“网络”面板显示 71.2 KB 的 JavaScript 软件包。

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

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

优化

您可以通过以下几种方式缩减软件包大小:

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

选项 1 和 2 是减小软件包大小的绝佳方法(可能最适合实际应用)。不过,为了教学目的,本教程中不会使用这些方法 😈。

选项 3 和 4 都有助于提升此应用的性能。此 Codelab 的后续几个部分将介绍这些步骤。与任何编码教程一样,请始终尝试自行编写代码,而不是复制和粘贴。

仅导入所需内容

需要修改一些文件,以便仅从 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>
  `
});

重新加载应用,打开开发者工具,然后再次查看网络面板。

“网络”面板显示 15.2 KB 的 JavaScript 软件包。

对于此应用,只需极少的工作,软件包大小就缩减了 4 倍以上,但仍有改进空间。

代码拆分

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

此应用中使用的单个 bundle 可以拆分为两个单独的块:

  • 一个负责构成初始路由的代码
  • 包含排序代码的次要分块

通过使用动态导入,可以延迟加载或按需加载次要分块。在此应用中,只有当用户按下按钮时,才能加载构成该分块的代码。

首先,移除 src/index.js 中 sort 方法的顶级导入:

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,当它解析时,系统会提供所选模块,该模块会被拆分为一个单独的块。返回模块后,module.default 用于引用 lodash 提供的默认导出。该 promise 与另一个 .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>
    `
  };
}

监控

最后一次重新加载应用,并再次密切关注网络面板。应用加载后,系统只会下载一个较小的初始 bundle。

“网络”面板显示 2.7 KB 的 JavaScript 软件包。

按下按钮对输入的数字进行排序后,系统会提取并执行包含排序代码的代码块。

“网络”面板,其中显示了一个 2.7 KB 的 JavaScript 软件包,后跟一个 13.9 KB 的 JavaScript 软件包。

请注意,数字仍然会排序!

总结

代码拆分和延迟加载是非常有用的技术,可以缩减应用的初始 bundle 大小,从而直接缩短网页加载时间。不过,在应用中加入此优化之前,您需要考虑一些重要事项。

延迟加载界面

延迟加载特定代码模块时,请务必考虑网络连接较弱的用户会获得怎样的体验。在用户提交操作时拆分并加载非常大的代码块,可能会让用户觉得应用可能已停止运行,因此请考虑显示某种加载指示器。

延迟加载第三方节点模块

在应用中延迟加载第三方依赖项并不总是最佳方法,这取决于您在何处使用它们。通常,第三方依赖项会拆分为一个单独的 vendor 软件包,该软件包可以缓存,因为它们不会经常更新。详细了解 SplitChunksPlugin 如何帮助您实现此目的。

使用 JavaScript 框架实现延迟加载

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

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

预加载和预提取

尽可能利用 <link rel="preload"> 或 <link rel="prefetch"> 等浏览器提示,尝试更快地加载关键模块。webpack 通过在 import 语句中使用魔法注释来支持这两种提示。预加载关键分块指南对此进行了更详细的说明。

延迟加载不仅仅适用于代码

图片可能构成应用的重要组成部分。延迟加载非首屏或设备视口之外的内容可以加快网站速度。如需了解详情,请参阅 Lazysizes 指南。