Tree Shaking
一、认识
Tree shaking
主要是借助ES6
模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量, 进而判断哪些模块已经加载, 哪些模块和变量未被使用或者引用,进而删除对应代码。Tree Shaking
用于去除 JavaScript
中未被使用的代码,减少最终打包文件的大小,提高应用加载速度。在基于 Webpack
的项目中,配置 Tree Shaking
主要通过以下几个步骤完成。
二、操作
2.1 确保使用 ESM
Tree Shaking
是基于 ES6
模块(import
和 export
)来工作的。CommonJS
和 AMD
模块格式不支持静态分析,因此不能有效地进行 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 Shaking
。sideEffects
是如何辅助 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 Modules
,Webpack
能够通过静态分析 import
和 export
语法来实现 Tree Shaking
,因为这些语法是静态的,Webpack
可以在编译时清晰地了解模块的依赖和导出。然而,CommonJS
模块的 require
和 module.exports
是动态的, 运行时同步加载。Webpack
很难静态分析这些模块,因此 Tree Shaking
效果通常较差。
3.1 将 CommonJS 转换为 ESM
如果你使用 Babel
来编译 JavaScript
,babel-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 Modules
的 Tree Shaking
行为,通过优化 CommonJS
模块中的导出和引用,使得未使用的代码能够被移除,从而减少打包后的文件体积。
webpack-common-shake
原理如下:
-
静态分析模块:
webpack-common-shake
插件会分析每个CommonJS
模块的exports
和require
。它会跟踪每个模块的导出内容,判断哪些导出部分在实际使用中被引用。 -
去除未引用的代码: 插件通过追踪依赖关系,去除没有被引用的代码块,这样即使是
CommonJS
模块,也能得到类似于ES
模块的Tree Shaking
优化。 -
动态导入的处理: 插件还尝试识别和处理动态导入模块(
require
和import()
),并尽可能地减少冗余代码。
webpack-common-shake
总结: webpack-common-shake
插件能够通过静态分析一定程度上优化这些情况,但它的效果通常较为有限。尤其是在动态 require
难以静态分析: require
是动态的,尤其是在条件语句、循环等结构中,这使得 Webpack
很难确定哪些代码应该被引入,哪些应该被丢弃。同时,对于多个 CommonJS
模块之间复杂的依赖关系,webpack-common-shake
插件可能无法完全解析依赖图,并准确去除未使用的代码。所以,webpack-common-shake
插件在一定程度上帮助 Webpack
对 CommonJS
模块进行 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
静态编译思想, 在编译时就能确定模块的依赖关系, 以及输入和输出的变量, 进而判断哪些模块已经加载, 哪些模块和变量未被使用或者引用,进而删除对应代码。构建工具(如 Webpack
、Rollup
)通过静态分析整个依赖图, 可以明确哪些导出的变量或函数被实际使用, 哪些未使用。构建工具会构建整个应用的模块依赖图, 从入口模块开始递归查找所有的依赖项。然后, 根据这个图确定每个模块中哪些代码是必需的、哪些代码可以安全地剔除。分析完成后, 打包工具将未被引用的代码从最终打包文件中移除。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";
拿到整个默认导出对象,然后通过解构取出A
、B
、C
。
-
-
Tree Shaking
分析:ES6
模块的Tree Shaking
依赖于静态分析导入与导出关系。对于命名导出, 构建工具可以明确知道哪些变量被使用, 从而在打包时剔除未被引用的代码。但是, 默认导出对象的情况不同: 整个对象在被导入时会被当作一个整体, 内部的属性(A
、B
、C
)没有单独的导出标识, 当B
模块通过解构来获取对象内的属性时, 这个过程是在运行时进行的, 构建工具无法在静态分析阶段确定只需要A
, 而剔除B
和C
。因此, 默认导出对象, 无法有效利用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
。 在这种情况下, 构建工具能够剔除B
和C
(假设模块内部无副作用)的未使用部分, 达到Tree Shaking
的效果。因此, 命名导出 支持Tree Shaking
。
4.4 SideEffects 辅助 Tree Shaking 的场景?
场景一、全局样式 Css
文件, 用来改变全局视觉表现的, 它们在加载时就会产生副作用。 Tree Shaking
主要依赖于静态分析模块的导入和导出, 但是全局样式文件中没有导出任何变量或函数, 如果不通过 sideEffects
声明来告知构建工具这些文件具有副作用, 它们可能会被误认为是 无用 的而在打包过程中被剔除, 导致样式丢失。
场景二、具有副作用的 Js
文件, 如果某个模块中, 声明某个全局变量, 比如 添加修改 Window
属性、注册事件监听器 等, 没有进行任何导出。Tree Shaking
主要依赖于静态分析模块的导入和导出, 如果不通过 sideEffects
声明来告知构建工具这些文件具有副作用, 它们可能会被误认为是 无用 的而在打包过程中被剔除, 导致 添加修改 Window
属性、注册事件监听器 失效。