通过代码拆分减少 JavaScript 载荷

大多数网页和应用由许多不同的部分组成。将 JavaScript 拆分成多个分块,而不是在第一个网页加载后立即发送构成应用的所有 JavaScript,这样可以提高页面性能。

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

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

测量

一如既往,在尝试进行任何优化之前,请务必先衡量网站的效果。

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

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

仅仅为了在一个简单的应用中对几个数字进行排序,就需要 71.2 KB 的 JavaScript。What gives?

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

优化

您可以通过以下几种方式来缩减 bundle 大小:

  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>
  `
});

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

显示 15.2 KB JavaScript 软件包的 Network 面板。

对于此应用,只需进行很少的工作,就可以将软件包大小缩减 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,并在解析后提供所选模块,该模块会拆分为一个单独的块。返回模块后,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 面板。应用加载后,系统只会下载一个小型初始 bundle。

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

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

“Network”面板,显示 2.7 KB 的 JavaScript 软件包,后跟 13.9 KB 的 JavaScript 软件包。

请注意数字仍然会按顺序排列!

总结

代码拆分和延迟加载是缩减应用初始软件包大小的极其有用的方法,这可以直接缩短网页加载时间。不过,在将此优化功能添加到应用中之前,需要考虑一些重要事项。

延迟加载界面

延迟加载特定代码模块时,请务必考虑网络连接较弱的用户的体验。在用户提交操作时拆分并加载大量代码可能会导致应用看起来已停止运行,因此请考虑显示某种加载指示器。

延迟加载第三方节点模块

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

使用 JavaScript 框架进行延迟加载

许多使用 webpack 的热门框架和库都提供了抽象功能,可让延迟加载变得更简单,比在应用中使用动态导入更为方便。

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

预加载和预提取

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

延迟加载的不仅仅是代码

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