tree shaking(摇树优化)
tree shaking是什么
在前端的性能优化中,ES6
推出了tree shaking
机制,tree shaking
就是当我们在项目中引入其他模块时,它会自动将我们没有用到的代码,或者永远不会执行的代码摇掉,在Uglify
阶段查出,不打包到bundle
中。
tree-shaking
可以理解为通过工具“摇”我们的 JS 文件,将其中用不到的代码“摇”掉,是一个性能优化的范畴。具体来说,在webpack
项目中,有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于树枝。实际情况下,虽然依赖了某个模块,但其实只使用其中的某些功能。通过tree-shaking
,将没有使用的模块code
摇掉,这样来达到删除无用代码的目的。
只支持ES6 Module
代码,在production
环境默认开启。
哪些情况下可以使用tree shaking呢?
首先,要明确一点:Tree shaking只支持ESM
的引入方式,不支持Common JS 的引入方式。
- ESM: export + import
- Common JS: module.exports + require
提示:如果想要做到tree shaking,在引入模块时就应该避免将全部引入,应该引入布局才可以触发tree shaking机制。
代码如下(demo):
// Import everything (not tree-shaking)
import lodash from "lodash"
// Import named export (can be tree-shaking)
import {debounce} from "lodash"
// Import the item directly (can be tree-shaking)
import debounce from "lodash/lib/debounce"
项目中如何配置 tree shaking?
- 开发环境配置tree shaking
// webpack.config.js
module.exports = {
// ....
mode: "development",
optimization: {
useExports: true
}
}
- 生产环境下的配置
// webpack.config.js 生产环境下只需要把mode配置成"production"即可。production环境下默认开启tree shaking
module.exports = {
// ...
mode: "production"
}
sideEffects: false
根据环境的不同进行配置以后,还需要在package.json
中,添加字段:sideEffects: false
,告诉webpack哪些代码可以处理。
{
"name": "webpacl-demo-1",
"sideEffects": false,
// .....
}
// demo:
// All files have side effects, and none can be tree-shaking
{
"sideEffects": true
}
// No files have side effects, all can be tree-shaken
{
"sideEffects": false
}
// Only these files have side effects, all other files can be tree-shaken, but these must be kept
{
"sideEffects": [
"./src/file1.js",
"./src/file2.ks"
]
}
sideEffects 对全局CSS的影响
- 对于那些直接引入到 js 文件的文件,例如全局的 css,它们并不会被转换成一个 CSS 模块
/* reset.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
background-color: #ffffff;
}
// main.js
import "./style/reset.css";
- 这样的代码,在打包后,打开页面,你就会发现样式并没有应用上,原因在于:上面我们将
sideEffects
设置为false
后,所有的文件都会被 Tree Shaking,通过import
这样的形式引入的 css 就会被当作无用的代码处理掉。 - 为了解决这个问题,可以在 loader 的规则配置中,添加
sideEffects: true
,告诉webpack这些文件不要Tree Shaking。
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
sideEffects: true
}
]
}
}
tree shaking的原理(webpack)
common.js 和 ES6中模块引入的区别
CommonJS
模块输出的是一个值的拷贝,ES6
模块输出的是值的引用CommonJS
模块时运行时加载,ES6
模块时编译时输出接口CommonJS
是单个值导出,ES6 Module
可以导出多个CommonJS
是动态语法可以写在判断里,ES6 Module
是静态语法,只能写在顶层CommonJS
的this
是当前模块,ES6 Module
的this
是undefined
Tree shaking的本质——消除无用的JavaScript代码
因为ES6 Model的出现,ES6 Module 依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析。
ES6 Module
引入静态分析,故而编译的时候正确判断到底加载了哪些模块- 静态分析程序流,判断哪些模块和变量未被使用或者引用,进而删除对应代码
Tree shaking实现原理
Make阶段,收集模块导出变量并记录到模块依赖关系图
ModuleGraph
变量中。将模块的所有
ES Module
导出语句转换为Dependency
对象,并记录到module
对象的denpencies
集合,转换规则:- 具名导出转换为
HarmonyExportSecifierDependency
对象 default
导出为HarmonyExportsExpressionDependency
对象
- 具名导出转换为
FlagDependencyExportsPlugin
插件的转换处理流程:
- 所有模块都编译完毕后,触发
compilation.hooks.finishModules
钩子,开始执行FlagDependencyExportsPlugin
插件回调 FlagDependencyExportsPlugin
插件从entry
开始读取ModuleGraph
中存储的模块信息,遍历所有module
对象- 遍历
module
对象的dependencies
数据,找到所有HarmonyXXXDependency
类型的依赖对象,将其转换为ExportInfo
对象并记录到ModuleGraph
体系中。
经过FlagDependencyExportsPlugin
插件处理后,所有ES Module
风格的export
语句都会记录在ModuleGraph
体系中,后续操作就可以从ModuleGraph
中直接读取出模块的导出值。
Seal阶段,遍历ModuleGraph标记模块导出变量有没有被使用
模块导出信息收集完毕后,Webpack需要标记出各个模块的导出列表,哪些导出值有被其它模块用到,哪些没有,这一过程发生在Seal阶段,主流程:
- 触发
compilation.hooks.optimizeDependencies
钩子,开始执行FlagDependencyUsagePlugin
插件逻辑 - 在
FlagDependencyUsagePlugin
插件中,从entry
开始逐步遍历ModuleGraph
存储的所有module
对象 - 遍历
module
对象对应的exportInfo
数据 - 为每一个
exportInfo
对象执行compilation.getDependencyReferencedExports
方法,确定其对应的dependency
对象有否被其它模块使用 - 被任意模块使用到的导出值,调用
exportInfo.setUsedConditionally
方法将其标记为已被使用。 exportInfo.setUsedConditionally
内部修改exportInfo._usedInRuntime
属性,记录该导出被如何使用。
上面是季度简化过的版本,中间还存在非常多的分支逻辑与复杂的集合操作,我们抓住重点:标记模块导出这一操作集中在
FlagDependencyUsagePlugin
插件中,执行结果最终会记录在模块导出语句对应的exportInfo._usedInRuntime
字典中。- 触发
生成产物时,若变量没有被其它模块使用则删除对应的导出语句
- 打包阶段,调用
HarmonuExportXXXDependency.Template.apply
方法生成代码 - 在
apply
方法内,读取ModuleGraph
中存储的exportsInfo
信息,判断哪些导出值被使用,哪些未被使用 - 对已经被使用及未被使用的导出值,分别创建对应的
HarmonyExportInitFragment
对象,保存到initFragments
数组。 - 遍历
initFragments
数组,生成最终结果。
- 打包阶段,调用
经过前面几步操作之后,模块导出列表中未被使用的值都不会定义在__webpack_exports__
对象中,形成一段不可能被执行的Dead Code
效果。在此之后,将由Terser
、UglifyJS
等DCE
工具“摇”掉这部分无效代码,构成完整的Tree Shaking
操作。
总结
- tree shaking就是类似一棵树有长熟的苹果,将已将长熟的苹果摇掉减轻树的负担,这就实现了这个机制。
- 在
ES6
中的import
和export
才可以触发这个机制 - 项目中对 tree-shaking的配置,production环境下默认是开启了tree-shaking机制的。
- tree-shaking对项目中的影响