tree+shaking(摇树优化)

发布于:2024-12-06 ⋅ 阅读:(26) ⋅ 点赞:(0)

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?

  1. 开发环境配置tree shaking
// webpack.config.js
module.exports = {
    // ....
    mode: "development",
    optimization: {
        useExports: true
    }
}
  1. 生产环境下的配置
// webpack.config.js 生产环境下只需要把mode配置成"production"即可。production环境下默认开启tree shaking
module.exports = {
    // ...
    mode: "production"
}
  1. 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中模块引入的区别
  1. CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用
  2. CommonJS模块时运行时加载,ES6模块时编译时输出接口
  3. CommonJS是单个值导出,ES6 Module可以导出多个
  4. CommonJS是动态语法可以写在判断里,ES6 Module是静态语法,只能写在顶层
  5. CommonJSthis是当前模块,ES6 Modulethisundefined
Tree shaking的本质——消除无用的JavaScript代码

因为ES6 Model的出现,ES6 Module 依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析。

  • ES6 Module引入静态分析,故而编译的时候正确判断到底加载了哪些模块
  • 静态分析程序流,判断哪些模块和变量未被使用或者引用,进而删除对应代码
Tree shaking实现原理
  • Make阶段,收集模块导出变量并记录到模块依赖关系图ModuleGraph变量中。

    将模块的所有ES Module导出语句转换为Dependency对象,并记录到module对象的denpencies集合,转换规则:

    • 具名导出转换为HarmonyExportSecifierDependency 对象
    • default导出为HarmonyExportsExpressionDependency对象

FlagDependencyExportsPlugin插件的转换处理流程:

  1. 所有模块都编译完毕后,触发compilation.hooks.finishModules钩子,开始执行FlagDependencyExportsPlugin插件回调
  2. FlagDependencyExportsPlugin插件从entry开始读取ModuleGraph中存储的模块信息,遍历所有module对象
  3. 遍历module对象的dependencies数据,找到所有HarmonyXXXDependency类型的依赖对象,将其转换为ExportInfo对象并记录到ModuleGraph体系中。

经过FlagDependencyExportsPlugin插件处理后,所有ES Module风格的export语句都会记录在ModuleGraph体系中,后续操作就可以从ModuleGraph中直接读取出模块的导出值。

  • Seal阶段,遍历ModuleGraph标记模块导出变量有没有被使用

    模块导出信息收集完毕后,Webpack需要标记出各个模块的导出列表,哪些导出值有被其它模块用到,哪些没有,这一过程发生在Seal阶段,主流程:

    1. 触发compilation.hooks.optimizeDependencies钩子,开始执行FlagDependencyUsagePlugin插件逻辑
    2. FlagDependencyUsagePlugin插件中,从entry开始逐步遍历ModuleGraph存储的所有module对象
    3. 遍历module对象对应的exportInfo数据
    4. 为每一个exportInfo对象执行compilation.getDependencyReferencedExports方法,确定其对应的dependency对象有否被其它模块使用
    5. 被任意模块使用到的导出值,调用exportInfo.setUsedConditionally方法将其标记为已被使用。
    6. exportInfo.setUsedConditionally内部修改exportInfo._usedInRuntime属性,记录该导出被如何使用。

    上面是季度简化过的版本,中间还存在非常多的分支逻辑与复杂的集合操作,我们抓住重点:标记模块导出这一操作集中在FlagDependencyUsagePlugin插件中,执行结果最终会记录在模块导出语句对应的exportInfo._usedInRuntime字典中。

  • 生成产物时,若变量没有被其它模块使用则删除对应的导出语句

    1. 打包阶段,调用HarmonuExportXXXDependency.Template.apply方法生成代码
    2. apply方法内,读取ModuleGraph中存储的exportsInfo信息,判断哪些导出值被使用,哪些未被使用
    3. 对已经被使用及未被使用的导出值,分别创建对应的HarmonyExportInitFragment对象,保存到initFragments数组。
    4. 遍历initFragments数组,生成最终结果。

经过前面几步操作之后,模块导出列表中未被使用的值都不会定义在__webpack_exports__对象中,形成一段不可能被执行的Dead Code效果。在此之后,将由TerserUglifyJSDCE工具“摇”掉这部分无效代码,构成完整的Tree Shaking操作。

总结

  1. tree shaking就是类似一棵树有长熟的苹果,将已将长熟的苹果摇掉减轻树的负担,这就实现了这个机制。
  2. ES6中的 importexport才可以触发这个机制
  3. 项目中对 tree-shaking的配置,production环境下默认是开启了tree-shaking机制的。
  4. tree-shaking对项目中的影响