tree+shaking(摇树优化)

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):

js 复制代码
// 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
js 复制代码
// webpack.config.js
module.exports = {
    // ....
    mode: "development",
    optimization: {
        useExports: true
    }
}
  1. 生产环境下的配置
js 复制代码
// webpack.config.js 生产环境下只需要把mode配置成"production"即可。production环境下默认开启tree shaking
module.exports = {
    // ...
    mode: "production"
}
  1. sideEffects: false

​ 根据环境的不同进行配置以后,还需要在package.json中,添加字段:sideEffects: false,告诉webpack哪些代码可以处理。

js 复制代码
{
    "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 模块
css 复制代码
/* reset.css */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
html,
body {
    background-color: #ffffff;
}
js 复制代码
// main.js
import "./style/reset.css";
  • 这样的代码,在打包后,打开页面,你就会发现样式并没有应用上,原因在于:上面我们将 sideEffects设置为 false后,所有的文件都会被 Tree Shaking,通过import这样的形式引入的 css 就会被当作无用的代码处理掉。
  • 为了解决这个问题,可以在 loader 的规则配置中,添加 sideEffects: true,告诉webpack这些文件不要Tree Shaking。
js 复制代码
// 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对项目中的影响
相关推荐
q***71851 天前
Webpack、Vite区别知多少?
前端·webpack·node.js
谢尔登1 天前
简单聊聊webpack摇树的原理
运维·前端·webpack
谢尔登1 天前
原来Webpack在大厂中这样进行性能优化!
前端·webpack·性能优化
醉方休3 天前
Webpack loader 的执行机制
前端·webpack·rust
带只拖鞋去流浪3 天前
迎接2026,重新认识Vue CLI (v5.x)
前端·vue.js·webpack
小奶包他干奶奶3 天前
Webpack学习——Loader(文件转换器)
前端·学习·webpack
小奶包他干奶奶3 天前
Webpack学习——原理理解
学习·webpack·devops
|晴 天|4 天前
Vite 为何能取代 Webpack?新一代构建工具的崛起
前端·webpack·node.js
带只拖鞋去流浪4 天前
迎接2026,重新认识Webpack5
前端·webpack
我也爱吃馄饨4 天前
写的webpack插件如何适配CommonJs项目和EsModule项目
java·前端·webpack