JS 加载性能 Tree Shaking 优化详解

作者:缓存笔记

时间:2026-10-25

浏览:582

本文深入解析 JavaScript 加载性能中的 Tree Shaking 技术。通过对比 JS 与图像资源的处理成本,阐述消除死代码对减小包体积的重要性。详细讲解如何在 Webpack 中利用 ES6 静态模块语法,配置 Babel 避免 CommonJS 转换,处理 side effects,并针对 Lodash 等第三方库进行针对性优化,最终实现显著的代码体积缩减。

文章目录

正文

随着 Web 应用复杂度的提升,JavaScript 代码文件的体积也在持续膨胀。截至 2021 年 9 月,httparchive 的统计数据显示,移动设备上传输的 JS 文件大小约为 447 KB,桌面端约为 495 KB。需要注意的是,这仅仅是网络传输阶段的体积,实际加载的 JS 代码量往往远大于此。

上图展示了 JavaScript 下载与运行的完整流程。

即便将传输体积压缩至 300 KB,浏览器仍需解析、编译并执行约 900 KB 的 JS 代码。图片下载后,浏览器仅需进行相对简单的解码;而 JS 必须经过解析、编译和执行,这使得处理 JS 资源比处理其他类型资源更为耗时。

上图对比了浏览器解析/编译 170 KB JS 的处理成本与同等大小 JPEG 图片的解码时间。尽管 JS 引擎性能不断提升,优化网站 JS 性能仍是开发者的核心任务。Code Splitting 是优化 JS 性能的手段之一,但它无法减少应用 JS 代码的总大小,因此我们需要引入 Tree Shaking 来进一步减小 JS 代码体积。

什么是 Tree Shaking

可以将应用程序想象成一棵树。实际使用的源代码和库对应树中绿色的活叶子,而死代码则如同秋季树上棕色的枯叶。为了去除枯叶,需要摇动树木使其掉落。Tree Shaking 的核心即消除死代码,下面通过一个应用程序演示这一概念。我们使用 ES6 静态模块语法来导入依赖项:

// 在这里导入了所有的数组处理方法
import arrayUtils from "array-utils";

在应用初期,依赖项可能较少,但随着功能增加,依赖项也会随之增多。更糟糕的是,旧的不使用的依赖项可能未被从代码库中移除,导致应用携带大量未使用的 JS 代码。Tree Shaking 通过解析文件中使用的 ES6 静态模块语句,分析哪些模块被导入,从而解决这一问题:

// 只导入部分方法
import { unique, implode, explode } from "array-utils";

上述导入示例与前例的区别在于,本例仅导入模块的特定部分,而非从“array-utils”模块导入全部内容。

寻找 Tree Shaking 的机会

为便于说明,这里展示一个使用 Webpack 的单页应用程序示例,演示 Tree Shaking 的工作原理。界面如下:

该程序打包后的代码分为两个文件,具体如下:

在任何应用中,你需要从静态导入语句中寻找 Tree Shaking 的机会。在示例程序 (FilterablePedalList.js) 中,你会看到如下导入语句:

import * as utils from "../../utils/utils";

文件中的此类导入语句值得注意,它表示从 utils 模块导入所有内容。关键问题是:你真的用到了所有内容吗?通过检查 FilterablePedalList.js 发现,实际仅使用了 utils.simpleSort:

if (this.state.sortBy === "model") {
  // Simple sort gets used here...
  json = utils.simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
  // ..and here...
  json = utils.simpleSort(json, "type", this.state.sortOrder);
} else {
  // ..and here.
  json = utils.simpleSort(json, "manufacturer", this.state.sortOrder);
}

我们现在开始做 Tree Shaking 优化

防止 Babel 将 ES6 模块转换为 CommonJS 模块

在大型应用中,Babel 是不可或缺的工具,但它会使 Tree Shaking 变得困难。如果你使用 babel-preset-env,它会自动将 ES6 模块转换为兼容性更广的 CommonJS 模块,即用 require 代替 import。对于 CommonJS 模块而言,进行 Tree Shaking 优化非常困难,因为 CommonJS 模块是动态的,构建阶段的 bundlers 难以分析其导出和导入内容。为避免 babel-preset-env 将 ES6 模块转换为 CommonJS 模块,我们可以采取以下措施:

{
  "presets": [
    ["env", {
      "modules": false
    }]
  ]
}

在 Babel-preset-env 配置中指定 "modules": false,即可让 Babel 按预期运行,允许 Webpack 分析依赖树并移除未使用的依赖。

留意 side effects

当函数修改了其作用域之外的内容时,我们称该函数具有 side effects(副作用)。side effects 同样适用于 ES6 模块。在执行 Tree Shaking 时,需留意模块是否存在 side effects。如果模块接受可预测的输入并输出同样可预测的结果,且不修改其作用域之外的任何内容,则认为该模块没有 side effects,对此类模块可放心进行 Tree Shaking。以下列举两个具有 side effects 的例子:

import './index.module.scss';
import './assets/shoot.svg';

若模块中出现上述 import 语句,则认为该模块具有 side effects,此时进行 Tree Shaking 需谨慎。默认情况下,Webpack 在执行 Tree Shaking 时,会认为 index.module.scss 与 assets/shoot.svg 未被使用,从而将其移除。若希望保留它们,需告知 Webpack 它们是 side effects:

{
  "name": "webpack-tree-shaking-example",
  "version": "1.0.0",
  "sideEffects": [
    "./index.module.scss",
    "./assets/shoot.svg"
  ]
}

在项目的 package.json 中配置 sideEffects 字段。sideEffects 字段也可设为 false,表示项目中不存在具有 side effects 的模块。

只导入你需要的

现已告知 Babel 不要将 ES6 模块转换为 CommonJS 模块,接下来需对导入语法进行微调,仅从 utils 模块引入所需的函数。在本指南示例中,我们仅需 simpleSort:

import { simpleSort } from "../../utils/utils";
if (this.state.sortBy === "model") {
  json = simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
  json = simpleSort(json, "type", this.state.sortOrder);
} else {
  json = simpleSort(json, "manufacturer", this.state.sortOrder);
}

现在我们已经完成了 Tree Shaking 的工作,以下是 Tree Shaking 之前 Webpack 打包生成的 JS 包大小:

                 Asset      Size  Chunks             Chunk Names
js/vendors.16262743.js  37.1 KiB       0  [emitted]  vendors
   js/main.797ebb8b.js  20.8 KiB       1  [emitted]  main

以下是 Tree Shaking 之后 Webpack 打包生成的 JS 包大小:

                 Asset      Size  Chunks             Chunk Names
js/vendors.45ce9b64.js  36.9 KiB       0  [emitted]  vendors
   js/main.559652be.js  8.46 KiB       1  [emitted]  main

main 文件体积下降明显,这是因为 Webpack 移除了不需要的 utils 方法。

更复杂的情况

在某些情况下,即使按上述步骤进行 Tree Shaking,Webpack 仍会将模块的所有内容打包到最终的 Chunk 中,例如 Lodash。

// 仍然会导入所有的内容
import { sortBy } from "lodash";
// 这只会导入 sortBy
import sortBy from "lodash/sortBy";

若使用第一种写法,还需安装 babel-plugin-lodash。如果使用第三方库,可查看该库的导出是否使用了 ES6 语法。若其导出使用 CommonJS 语法(如 module.exports),则 Webpack 无法对其进行 Tree Shaking 优化。部分插件(如 webpack-common-shake)提供了对 CommonJS 模块进行 Tree Shaking 的能力,但存在一定限制。

总结

为确保构建工具能成功优化应用程序,应避免依赖 CommonJS 模块,并在整个应用中使用 ES6 模块语法。

以上就是 JS 加载性能 Tree Shaking 优化详解的详细内容,更多关于 JS 加载 Tree Shaking 的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章