跳到主要内容

Tree Shaking

2025年01月09日
柏拉文
越努力,越幸运

一、认识


Tree shaking 主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量, 进而判断哪些模块已经加载, 哪些模块和变量未被使用或者引用,进而删除对应代码。Tree Shaking 用于去除 JavaScript 中未被使用的代码,减少最终打包文件的大小,提高应用加载速度。在基于 Webpack 的项目中,配置 Tree Shaking 主要通过以下几个步骤完成。

二、操作


2.1 确保使用 ESM

Tree Shaking 是基于 ES6 模块(importexport)来工作的。CommonJSAMD 模块格式不支持静态分析,因此不能有效地进行 Tree Shaking

2.2 启用生产环境构建

Tree Shaking 主要作用在生产环境中。Webpack 会在生产模式下自动移除未使用的代码(例如,unused 的函数、变量等)。因此,确保 Webpack 配置中的 mode 设置为 production

module.exports = {
mode: 'production', // 开启生产模式
...
};

2.3 配置 optimization.usedExports

Webpack 中,optimization 配置项会影响到 Tree Shaking 和代码压缩。你需要确保以下设置:

  • usedExports:确保 Webpack 知道哪些导出是被使用的。

  • minimize:启用代码压缩,这可以帮助进一步去除未使用的代码。

module.exports = {
mode: 'production',
optimization: {
usedExports: true, // 启用 Tree Shaking
minimize: true, // 启用代码压缩
// 可选:启用内联的最小化代码
minimizer: [
// 使用 TerserPlugin 进行压缩
new TerserPlugin({
terserOptions: {
ecma: 2015, // 使用 ES2015 的特性进行压缩
},
}),
],
},
};

2.4 配置 package.json.sideEffects

Tree Shaking 是基于静态分析的,它会去除那些不被使用的代码。如果某个模块或文件在执行时有副作用(例如修改全局变量、日志输出等),即使它没有被显式使用,Webpack 也可能无法完全移除它。这是因为 Webpack 无法确定这些副作用是否会在其他地方使用。在 package.json 中使用 sideEffects 字段来指明哪些文件是有副作用的,Webpack 会根据这个字段来优化 Tree ShakingsideEffects 是如何辅助 Webpack 进行优化的?: 当 Webpack 打包时,它会通过静态分析来确定哪些导入的模块实际上被使用了,然后只保留这些被使用的代码,并将未使用的代码从最终的打包文件中删除,在这个过程中,Webpack 会检查模块的 sideEffects 字段, 如果一个模块具有 sideEffects 字段,并且设置为 false 会认为该模块没有副作用, Webpack 会在摇树优化过程中将未使用的导出从该模块中删除,因为它不会影响项目的功能。然而,如果一个模块具有 sideEffects 字段,并且设置为 true 或是一个数组, Webpack 会认为该模块具有副作用。在摇树优化过程中,Webpack 会保留该模块的所有导出,因为它不能确定哪些代码是副作用的, 这样可以确保项目中需要的副作用代码不会被误删除。

{
"sideEffects": [
"src/styles.css", // 告诉 Webpack 该 CSS 文件有副作用,不应该被 Tree Shaking 去除
"*.css", // 或者标明所有 CSS 文件都有副作用
]
}

三、CommonJS Tree Shaking


许多较老或以 CommonJS 格式发布的第三方库并不支持 Tree Shaking。即使你正确配置了 Webpack,如果你的依赖库没有使用 ES6 模块或正确的导出方式,Webpack 也无法进行有效的 Tree Shaking

Webpack 中,Tree Shaking 是一种优化机制,通过静态分析模块的引用,去除未使用的代码。对于 ES ModulesWebpack 能够通过静态分析 importexport 语法来实现 Tree Shaking,因为这些语法是静态的,Webpack 可以在编译时清晰地了解模块的依赖和导出。然而,CommonJS 模块的 requiremodule.exports 是动态的, 运行时同步加载。Webpack 很难静态分析这些模块,因此 Tree Shaking 效果通常较差。

3.1 将 CommonJS 转换为 ESM

如果你使用 Babel 来编译 JavaScriptbabel-plugin-transform-modules-commonjs 插件可以将 ES Modules 转换为 CommonJS 格式,这样有时能解决一些 Tree Shaking 的问题,尤其是当你使用的第三方库没有提供 ES 模块版本时。

{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
modules: "commonjs", // 转换为 CommonJS 模块
targets: "defaults", // 可以根据项目的目标环境调整
},
],
],
plugins: ["@babel/plugin-transform-runtime"]
},
}
}

3.2 模拟 ESM 模块的 Tree Shaking

webpack-common-shake 是一个针对 CommonJS 模块优化的插件,它通过分析模块中的导出和引用,尽量减少打包文件中的冗余代码。这个插件的目的是模拟 ES 模块的 Tree Shaking,尽可能地优化 CommonJS 模块。

webpack-common-shake 是一个针对 Webpack 的插件,旨在提升 CommonJS 模块的 Tree Shaking 效率,尽管 Webpack 本身对 CommonJS 模块的 Tree Shaking 支持有限。该插件的核心目标是模拟 ES ModulesTree Shaking 行为,通过优化 CommonJS 模块中的导出和引用,使得未使用的代码能够被移除,从而减少打包后的文件体积。

webpack-common-shake 原理如下:

  1. 静态分析模块webpack-common-shake 插件会分析每个 CommonJS 模块的 exportsrequire。它会跟踪每个模块的导出内容,判断哪些导出部分在实际使用中被引用。

  2. 去除未引用的代码: 插件通过追踪依赖关系,去除没有被引用的代码块,这样即使是 CommonJS 模块,也能得到类似于 ES 模块的 Tree Shaking 优化。

  3. 动态导入的处理: 插件还尝试识别和处理动态导入模块(requireimport()),并尽可能地减少冗余代码。

webpack-common-shake 总结: webpack-common-shake 插件能够通过静态分析一定程度上优化这些情况,但它的效果通常较为有限。尤其是在动态 require 难以静态分析: require 是动态的,尤其是在条件语句、循环等结构中,这使得 Webpack 很难确定哪些代码应该被引入,哪些应该被丢弃。同时,对于多个 CommonJS 模块之间复杂的依赖关系,webpack-common-shake 插件可能无法完全解析依赖图,并准确去除未使用的代码。所以,webpack-common-shake 插件在一定程度上帮助 WebpackCommonJS 模块进行 Tree Shaking,但其效果远不如原生支持 ES Modules。对于依赖 CommonJS 的项目,webpack-common-shake 可以优化一些冗余代码,减小文件体积,但并不能像 ES Modules 那样完全利用静态分析来实现高效的 Tree Shaking。因此,最佳的做法仍然是尽量使用 ES Modules,或者将 CommonJS 模块转换为 ES 模块,以便 Webpack 更好地进行优化。

安装:

npm install webpack-common-shake --save-dev

使用:

const CommonShakePlugin = require('webpack-common-shake');

module.exports = {
plugins: [
new CommonShakePlugin()
],
optimization: {
usedExports: true, // 启用用于去除未使用的代码
}
};

四、问题


4.1 如何开启 Tree Shaking?

4.2 Tree Shaking 的原理是什么?

Tree Shaking 基于 ES6 Module 静态编译思想, 在编译时就能确定模块的依赖关系, 以及输入和输出的变量, 进而判断哪些模块已经加载, 哪些模块和变量未被使用或者引用,进而删除对应代码。构建工具(如 WebpackRollup)通过静态分析整个依赖图, 可以明确哪些导出的变量或函数被实际使用, 哪些未使用。构建工具会构建整个应用的模块依赖图, 从入口模块开始递归查找所有的依赖项。然后, 根据这个图确定每个模块中哪些代码是必需的、哪些代码可以安全地剔除。分析完成后, 打包工具将未被引用的代码从最终打包文件中移除。Tree Shaking 的效果在一定程度上依赖于模块是否存在副作用(Side Effects), 如果某个模块在加载时会产生副作用(例如修改全局变量、注册事件监听、全局样式文件等), 即使它的导出内容没有被直接引用, 也不能被随意剔除。因此, 我们需要在 package.json 中使用 sideEffects 字段来指明哪些文件是有副作用的, Webpack 会根据这个字段来优化 Tree Shaking

4.3 Tree Shaking 有效运用场景?

场景一、默认导出对象

  • 描述:

    • A 模块: 使用 export default { A, B, C } 将三个变量以对象字面量形式默认导出。

    • B 模块: 通过 import AModule from "./A"; 拿到整个默认导出对象,然后通过解构取出 ABC

  • Tree Shaking 分析: ES6 模块的 Tree Shaking 依赖于静态分析导入与导出关系。对于命名导出, 构建工具可以明确知道哪些变量被使用, 从而在打包时剔除未被引用的代码。但是, 默认导出对象的情况不同: 整个对象在被导入时会被当作一个整体, 内部的属性(ABC)没有单独的导出标识, 当 B 模块通过解构来获取对象内的属性时, 这个过程是在运行时进行的, 构建工具无法在静态分析阶段确定只需要 A, 而剔除 BC。因此, 默认导出对象, 无法有效利用 Tree Shaking

场景二、命名导出和再导出

  • 描述:

    • A 模块: 分别使用 export A; export B; export C; 导出各个变量。

    • B 模块: 使用 import * as AModule from "./A"; 导入所有命名导出(通常之后会对部分导出进行再导出)。

    • C 模块: 从 B 模块引入时采用 import { A } from "./"; 只使用 A

  • Tree Shaking 分析: 命名导出天然支持静态分析, 构建工具能够明确知道哪些导出的变量被引用。如果 B 模块对 A 模块的导出进行了 再导出(例如使用 export const A = AModule.A; 或直接 export { A, B, C } from "./A";, 那么最终 C 模块只使用 A。 在这种情况下, 构建工具能够剔除 BC(假设模块内部无副作用)的未使用部分, 达到 Tree Shaking 的效果。因此, 命名导出 支持 Tree Shaking

4.4 SideEffects 辅助 Tree Shaking 的场景?

场景一、全局样式 Css 文件, 用来改变全局视觉表现的, 它们在加载时就会产生副作用。 Tree Shaking 主要依赖于静态分析模块的导入和导出, 但是全局样式文件中没有导出任何变量或函数, 如果不通过 sideEffects 声明来告知构建工具这些文件具有副作用, 它们可能会被误认为是 无用 的而在打包过程中被剔除, 导致样式丢失。

场景二、具有副作用的 Js 文件, 如果某个模块中, 声明某个全局变量, 比如 添加修改 Window 属性注册事件监听器 等, 没有进行任何导出。Tree Shaking 主要依赖于静态分析模块的导入和导出, 如果不通过 sideEffects 声明来告知构建工具这些文件具有副作用, 它们可能会被误认为是 无用 的而在打包过程中被剔除, 导致 添加修改 Window 属性注册事件监听器 失效。