阅读 477

Tree-shaking详解(webpack tree shaking原理)

一. 什么是 tree-shaking

前端中的 tree-shaking 可以理解为通过工具"摇"我们的 JS 文件,将其中用不到的代码"摇"掉,是一个性能优化的范畴。具体来说,在 webpack 项目中,有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于树枝。实际情况中,虽然依赖了某个模块,但其实只使用其中的某些功能。通过 tree-shaking,将没有使用的模块 code 摇掉,这样来达到删除无用代码的目的。

二. tree-shaking 的原理 (webpack)

  • common.js 和 es6 中模块引入的区别?

    1、CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

    2、CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

    3、CommonJs 是单个值导出,ES6 Module可以导出多个

    4、CommonJs 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层

    5、CommonJs 的 this 是当前模块,ES6 Module的 this 是 undefined

  • Tree shaking 的本质 - 消除无用的JavaScript代码

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

    • ES6 Module引入进行静态分析,故而编译的时候正确判断到底加载了那些模块

    • 静态分析程序流,判断那些模块和变量未被使用或者引用,进而删除对应代码

  • Tree shaking 实现原理

    • 具名导出转换为 HarmonyExportSpecifierDependency 对象

    • default 导出转换为 HarmonyExportExpressionDependency 对象

    • Make 阶段,收集模块导出变量并记录到模块依赖关系图 ModuleGraph 变量中。

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

企业微信截图_16369664146252.png

    `FlagDependencyExportsPlugin` 插件的转换处理流程:

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

    经过 `FlagDependencyExportsPlugin` 插件处理后,所有 Es Moudle 风格的 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.  打包阶段,调用 `HarmonyExportXXXDependency.Template.apply` 方法生成代码
    2.  在 `apply` 方法内,读取 ModuleGraph 中存储的 `exportsInfo` 信息,判断哪些导出值被使用,哪些未被使用
    3.  对已经被使用及未被使用的导出值,分别创建对应的 `HarmonyExportInitFragment` 对象,保存到 `initFragments` 数组
    4.  遍历 `initFragments` 数组,生成最终结果。

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

三. tree-shaking 实践

  • development 模式下开启 tree-shaking

企业微信截图_16369636453064.png

由 optimization.usedExports 收集未使用的导出内容的信息,并将其标记。

optimization.sideEffects 告知 webpack 去辨识 package.json 中的 [副作用](<https://github.com/webpack/webpack/blob/master/examples/side-effects/README.md>) 标记或规则,以跳过那些当导出不被使用且被标记不包含副作用的模块。

presets: [["es2015", { modules: false }]] 来设置导出模块为 es6 Moudle。

使用 uglifyjs-webpack-plugin 清楚标记的无用代码。

企业微信截图_16369630187057.png

  • production 模式下开启 tree-shaking

企业微信截图_1636963213687.png

在 development 模式下,为了开发和调试方便,我们是不会开启压缩的,而 production 下,会自动为我们开启 tree-shaking。去掉 usedExports 和 uglifyjs-webpack-plugin 相关配置,将 mode 修改为 production。

四. 无效的 tree-shaking

  • UglifyJS不能消除未引用的类,uglify不进行程序流分析,所以不能排除有可能有副作用的代码

    函数的参数若是引用类型,对于它属性的操作,都是有可能会产生副作用的。因为首先它是引用类型,对它属性的任何修改其实都是改变了函数外部的数据。其次获取或修改它的属性,会触发getter或者setter,而gettersetter是不透明的,有可能会产生副作用。uglify没有完善的程序流分析。它可以简单的判断变量后续是否被引用、修改,但是不能判断一个变量完整的修改过程,不知道它是否已经指向了外部变量,所以很多有可能会产生副作用的代码,都只能保守的不删除。rollup有程序流分析的功能,可以更好的判断代码是否真正会产生副作用。

  • 立即执行函数 IIFE

五. 如何避免无效的 tree-shaking

  • 尽量不写带有副作用的代码。诸如编写了立即执行函数,在函数里又使用了外部变量等。

  • 如果对 ES6 语义特性要求不是特别严格,可以开启 Babel 的loose模式,这个要根据自身项目判断,如:是否真的要不可枚举class的属性。

  • 如果是开发 JavaScript 库,请使用 rollup。并且提供 ES6 module 的版本,入口文件地址设置到package.json 的module字段。

  • 如果 JavaScript 库开发中,难以避免的产生各种副作用代码,可以将功能函数或者组件,打包成单独的文件或目录,以便于用户可以通过目录去加载。如有条件,也可为自己的库开发单独的webpack-loader,便于用户按需加载。

  • 如果是工程项目开发,对于依赖的组件,只能看组件提供者是否有对应上述3、4点的优化。对于自身的代码,除1、2两点外,对于项目有极致要求的话,可以先进行打包,最终再进行编译。

  • 如果对项目非常有把握,可以通过 uglify 的一些编译配置,如:pure_getters: true,删除一些强制认为不会产生副作用的代码。


作者:一二三四五917
链接:https://juejin.cn/post/7030727223602905119


文章分类
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐