精华内容
下载资源
问答
  • Tree-Shaking原理

    千次阅读 2020-02-10 22:42:25
    Tree-Shaking性能优化实践 - 原理篇 一. 什么是Tree-shaking 先来看一下Tree-shaking原始的本意 上图形象的解释了Tree-shaking 的本意,本文所说的前端中的tree-shaking可以理解为通过工具"摇"我们的JS...

     

    Tree-Shaking性能优化实践 - 原理篇

     

    一. 什么是Tree-shaking

    先来看一下Tree-shaking原始的本意

     

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

     

    Tree-shaking 较早由 Rich_Harris 的 rollup 实现,后来,webpack2 也增加了tree-shaking 的功能。其实在更早,google closure compiler 也做过类似的事情。三个工具的效果和使用各不相同,使用方法可以通过官网文档去了解,三者的效果对比,后文会详细介绍。

     

    二. tree-shaking的原理

    Tree-shaking的本质是消除无用的js代码。无用代码消除在广泛存在于传统的编程语言编译器中,编译器可以判断出某些代码根本不影响输出,然后消除这些代码,这个称之为DCE(dead code elimination)。

    Tree-shaking 是 DCE 的一种新的实现,Javascript同传统的编程语言不同的是,javascript绝大多数情况需要通过网络进行加载,然后执行,加载的文件大小越小,整体执行时间更短,所以去除无用代码以减少文件体积,对javascript来说更有意义。

    Tree-shaking 和传统的 DCE的方法又不太一样,传统的DCE 消灭不可能执行的代码,而Tree-shaking 更关注宇消除没有用到的代码。下面详细介绍一下DCE和Tree-shaking。

     

    (1)先来看一下DCE消除大法

     

    Dead Code 一般具有以下几个特征

    •代码不会被执行,不可到达

    •代码执行的结果不会被用到

    •代码只会影响死变量(只写不读)

     

    下面红框标示的代码就属于死码,满足以上特征

    图4

    传统编译型的语言中,都是由编译器将Dead Code从AST(抽象语法树)中删除,那javascript中是由谁做DCE呢?

    首先肯定不是浏览器做DCE,因为当我们的代码送到浏览器,那还谈什么消除无法执行的代码来优化呢,所以肯定是送到浏览器之前的步骤进行优化。

    其实也不是上面提到的三个工具,rollup,webpack,cc做的,而是著名的代码压缩优化工具uglify,uglify完成了javascript的DCE,下面通过一个实验来验证一下。

     

    以下所有的示例代码都能在我们的github中找到,欢迎戳❤

    github.com/lin-xi/tree…

     

    分别用rollup和webpack将图4中的代码进行打包

    图5

    中间是rollup打包的结果,右边是webpack打包的结果

    可以发现,rollup将无用的代码foo函数和unused函数消除了,但是仍然保留了不会执行到的代码,而webpack完整的保留了所有的无用代码和不会执行到的代码。

     

    分别用rollup + uglify和 webpack + uglify 将图4中的代码进行打包

    图6

    中间是配置文件,右侧是结果

    可以看到右侧最终打包结果中都去除了无法执行到的代码,结果符合我们的预期。

     

    (2) 再来看一下Tree-shaking消除大法

    前面提到了tree-shaking更关注于无用模块的消除,消除那些引用了但并没有被使用的模块。

    先思考一个问题,为什么tree-shaking是最近几年流行起来了?而前端模块化概念已经有很多年历史了,其实tree-shaking的消除原理是依赖于ES6的模块特性。

    ES6 module 特点:

    • 只能作为模块顶层的语句出现
    • import 的模块名只能是字符串常量
    • import binding 是 immutable的

    ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础。

    所谓静态分析就是不执行代码,从字面量上对代码进行分析,ES6之前的模块化,比如我们可以动态require一个模块,只有执行后才知道引用的什么模块,这个就不能通过静态分析去做优化。

    这是 ES6 modules 在设计时的一个重要考量,也是为什么没有直接采用 CommonJS,正是基于这个基础上,才使得 tree-shaking 成为可能,这也是为什么 rollup 和 webpack 2 都要用 ES6 module syntax 才能 tree-shaking。

     

    我们还是通过例子来详细了解一下

    面向过程编程函数和面向对象编程是javascript最常用的编程模式和代码组织方式,从这两个方面来实验:

    • 函数消除实验
    • 类消除实验

    先看下函数消除实验

    utils中get方法没有被使用到,我们期望的是get方法最终被消除。

    注意,uglify目前不会跨文件去做DCE,所以上面这种情况,uglify是不能优化的。

    先看看rollup的打包结果

    完全符合预期,最终结果中没有get方法

    再看看webpack的结果

    也符合预期,最终结果中没有get方法

    可以看到rollup打包的结果比webpack更优化

    函数消除实验中,rollup和webpack都通过,符合预期

     

    再来看下类消除实验

    增加了对menu.js的引用,但其实代码中并没有用到menu的任何方法和变量,所以我们的期望是,最终代码中menu.js里的内容被消除

    main.js

    menu.js

    rollup打包结果

    包中竟然包含了menu.js的全部代码

    webpack打包结果

    包中竟然也包含了menu.js的全部代码

    类消除实验中,rollup,webpack 全军覆没,都没有达到预期

    what happend?

    这跟我们想象的完全不一样啊?为什么呢?无用的类不能消除,这还能叫做tree-shaking吗?我当时一度怀疑自己的demo有问题,后来各种网上搜索,才明白demo没有错。

    下面摘取了rollup核心贡献者的的一些回答:

    图7

    • rollup只处理函数和顶层的import/export变量,不能把没用到的类的方法消除掉
    • javascript动态语言的特性使得静态分析比较困难
    • 图7下部分的代码就是副作用的一个例子,如果静态分析的时候删除里run或者jump,程序运行时就可能报错,那就本末倒置了,我们的目的是优化,肯定不能影响执行

     

    再举个例子说明下为什么不能消除menu.js,比如下面这个场景

    function Menu() {
    }
    
    Menu.prototype.show = function() {
    }
    
    Array.prototype.unique = function() {
        // 将 array 中的重复元素去除
    }
    
    export default Menu;
    复制代码

    如果删除里menu.js,那对Array的扩展也会被删除,就会影响功能。那也许你会问,难道rollup,webpack不能区分是定义Menu的proptotype 还是定义Array的proptotype吗?当然如果代码写成上面这种形式是可以区分的,如果我写成这样呢?

    function Menu() {
    }
    
    Menu.prototype.show = function() {
    }
    
    var a = 'Arr' + 'ay'
    var b
    if(a == 'Array') {
        b = Array
    } else {
        b = Menu
    }
    
    b.prototype.unique = function() {
        // 将 array 中的重复元素去除
    }
    
    export default Menu;
    复制代码

    这种代码,静态分析是分析不了的,就算能静态分析代码,想要正确完全的分析也比较困难。

    更多关于副作用的讨论,可以看这个

    图标

    Tree shaking class methods · Issue #349 · rollup/rollupgithub.com

     

    tree-shaking对函数效果较好

    函数的副作用相对较少,顶层函数相对来说更容易分析,加上babel默认都是"use strict"严格模式,减少顶层函数的动态访问的方式,也更容易分析

     

    我们开始说的三个工具,rollup和webpack表现不理想,那closure compiler又如何呢?

    将示例中的代码用cc打包后得到的结果如下:

    天啊,这不就是我们要的结果吗?完美消除所有无用代码的结果,输出的结果非常性感

    closure compiler, tree-shaking的结果完美!

    可是不能高兴得太早,能得到这么完美结果是需要条件的,那就是cc的侵入式约束规范。必须在代码里添加这样的代码,看红线框标示的

    google定义一整套注解规范Annotating JavaScript for the Closure Compiler,想更多了解的,可以去看下官网。

    侵入式这个就让人很不爽,google Closure Compiler是java写的,和我们基于node的各种构建库不可能兼容(不过目前好像已经有nodejs版 Closure Compiler),Closure Compiler使用起来也比较麻烦,所以虽然效果很赞,但比较难以应用到项目中,迁移成本较大。

     

    说了这么多,总结一下:

    三大工具的tree-shaking对于无用代码,无用模块的消除,都是有限的,有条件的。closure compiler是最好的,但与我们日常的基于node的开发流很难兼容。

    tree-shaking对web意义重大,是一个极致优化的理想世界,是前端进化的又一个终极理想。

    理想是美好的,但目前还处在发展阶段,还比较困难,有各个方面的,甚至有目前看来无法解

    决的问题,但还是应该相信新技术能带来更好的前端世界。

    优化是一种态度,不因小而不为,不因艰而不攻。

     

    知识有限,如果错误,请不惜指正,谢谢

     

    下一篇将继续介绍 Tree-Shaking性能优化实践 - 实践

     

    展开全文
  • 主要介绍了浅谈Webpack4 Tree Shaking 终极优化指南,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
  • tree shaking 及其工作原理

    千次阅读 2021-02-15 15:31:41
    写在前面今天这道题目是在和小红书的一位面试官聊的时候:我:如果要你选择一道题目来考察面试者,你最有可能选择哪一道?面试官:那应该就是介绍一下tree shaking及其工作原理?我:为什么...

    写在前面

    今天这道题目是在和小红书的一位面试官聊的时候:

    我:如果要你选择一道题目来考察面试者,你最有可能选择哪一道?

    面试官:那应该就是介绍一下tree shaking及其工作原理?

    我:为什么?

    面试官:是因为最近面了好多同学,大家都说熟悉webpack,在项目中如何去使用、如何去优化,也都或多或少会提到tree shaking,但是每当我深入去问其工作机制或者原理时,却少有人能回答上来。(小声 bb:并不是我想内卷,确实是工程师的基本素养啊,哈哈 ????)

    面试官:那你来回答一下这个问题?

    我:我也用过tree shaking,只是知道它的别名叫树摇,最早是由Rollup实现,是一种采用删除不需要的额外代码的方式优化代码体积的技术。但是关于它的原理,我还真的不知道,额,,,,

    我们平时更多时候是停留在应用层面,这种只是能满足基础的业务诉求,对于后期的技术深挖以及个人的职业发展都是受限的。还是那句老话:知其然,更要知其所以然~

    话不多说,下面我就带大家一起来深入探究这个问题。

    什么是Tree shaking

    Tree shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination

    这个概念,我相信大多数同学都是了解的。什么,你不懂?

    不懂没关系,我可以教你啊(不过那是另外的价钱,哈哈 ????)

    走远了,兄弟,让我们言归正传:tree shaking如何工作的呢?

    tree shaking如何工作的呢?

    虽然 tree shaking 的概念在 1990 就提出了,但直到 ES6ES6-style 模块出现后才真正被利用起来。

    ES6以前,我们可以使用CommonJS引入模块:require(),这种引入是动态的,也意味着我们可以基于条件来导入需要的代码:

    let dynamicModule;
    // 动态导入
    if (condition) {
      myDynamicModule = require("foo");
    } else {
      myDynamicModule = require("bar");
    }
    

    但是CommonJS规范无法确定在实际运行前需要或者不需要某些模块,所以CommonJS不适合tree-shaking机制。在 ES6 中,引入了完全静态的导入语法:import。这也意味着下面的导入是不可行的:

    // 不可行,ES6 的import是完全静态的
    if (condition) {
      myDynamicModule = require("foo");
    } else {
      myDynamicModule = require("bar");
    }
    

    我们只能通过导入所有的包后再进行条件获取。如下:

    import foo from "foo";
    import bar from "bar";
    
    if (condition) {
      // foo.xxxx
    } else {
      // bar.xxx
    }
    

    ES6import语法可以完美使用tree shaking,因为可以在代码不运行的情况下就能分析出不需要的代码。

    看完上面的分析,你可能还是有点懵,这里我简单做下总结:因为tree shaking只能在静态modules下工作。ECMAScript 6 模块加载是静态的,因此整个依赖树可以被静态地推导出解析语法树。所以在 ES6 中使用 tree shaking 是非常容易的。

    tree shaking的原理是什么?

    看完上面的分析,相信这里你可以很容易的得出题目的答案了:

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

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

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

    但到这里,本篇文章还没结束。从这道题目我们可以很容易的引申出来另外一道“明星”面试题:common.js 和 es6 中模块引入的区别?

    这道题目来自冴羽大佬的阿里前端攻城狮们写了一份前端面试题答案,请查收[1]

    这里就直接贴下他给出的答案了:

    CommonJS 是一种模块规范,最初被应用于 Nodejs,成为 Nodejs 的模块规范。运行在浏览器端的 JavaScript 由于也缺少类似的规范,在 ES6 出来之前,前端也实现了一套相同的模块规范 (例如: AMD),用来对前端模块进行管理。自 ES6 起,引入了一套新的 ES6 Module 规范,在语言标准的层面上实现了模块功能,而且实现得相当简单,有望成为浏览器和服务器通用的模块解决方案。但目前浏览器对 ES6 Module 兼容还不太好,我们平时在 Webpack 中使用的 exportimport,会经过 Babel 转换为 CommonJS 规范。在使用上的差别主要有:

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

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

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

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

    5、CommonJsthis 是当前模块,ES6 Modulethisundefined

    冴羽大佬的文章质量都非常高,也欢迎大家多去支持冴羽大佬,相信看完一定会对你有所收获。

    总结一下

    这是大厂面试问题解析的第二篇了,和之前准备写这一系列的初衷一样:我力求通过一些面试题去发掘自己未曾了解或者未曾深入了解的一个领域。

    面试题更多时候是一个引子,更多是想通过面试题去思考题目背后带来的对某一模块的深入学习和探讨。

    当然,每篇文章也不会只是草草给出答案,我都会尽量深入浅出的给出自己对于这道题目的理解,也会在这个基础上做一些拓展。

    参考资料

    [1]

    阿里前端攻城狮们写了一份前端面试题答案,请查收: https://juejin.cn/post/6844904097556987917

    展开全文
  • webpack构建之tree-shaking原理是什么

    千次阅读 2020-08-20 17:53:03
    我们在开发一个项目的时候,总会遇到这样的问题,就是比如我们写了一个utils工具类,我们在某一个组件内要用到utils这个类里的其中一个或者某几个方法,但是当我们引入utils的时候,实际是...tree-shaking原理 利用es6

    我们在开发一个项目的时候,总会遇到这样的问题,就是比如我们写了一个utils工具类,我们在某一个组件内要用到utils这个类里的其中一个或者某几个方法,但是当我们引入utils的时候,实际是将utils里的方法全都引入了,这样就会导致将没有必要的东西也引入,包提就会越来越大。那么我们如何解决这个问题呢?是的,tree-shaking.看名字就知道是将哪些没有用的东西都shaking掉。Tree-shaking是一种通过清楚多余代码的方式来优化项目打包体积的技术。

    tree-shaking原理

    利用es6模块的特点:

    • 只能作为模块顶层的语句出现
    • import的模块名只能是字符串常量,不能动态引入
    • import binding是immutable,引入的模块不能再做修改

    其实tree-shaking的概念很早就提出了,但是直到es6的ES6-style模块出现后才被真正的利用起来,这是因为tree-shaking只能在静态modules下工作,Es6模块的加载是静态的。因此整个依赖树可以被静态的推导出解析语法树,所以在es6模块中使用tree-shaking是非常容易的。而且也支持statement(声明级别)。

    之前,我们可以使用commonjs引入模块。require(),这种引入是动态的,也就是意味着我们可以给予条件来导入需要的代码:

    let mynamicModule
    if (condition) {
        mynamicModule = require('aaa')
    } else {
        mynamicModule = require('bbb')
    }

    commonjs的动态特性模块意味着tree-shaking不适用,因为它是不可能确定哪些模块实际运行之前是需要或者是不需要的。在ES6中,进入来完全静态的导入语法:import,这也就是意味着下面的导入是不可行的:

    if (condition) {
        mynamicModule = require('aaa')
    } else {
        mynamicModule = require('bbb')
    }

    只能是通过导入所有的包后再进行有条件的获取:

    import aaa from './aaa'
    import bbb from './bbb'
    if (condition) {
    
    } else {
    
    }

    es6的import语法完美可以使用tree-shaking,因为可以在代码不运行的情况下就能分析出不需要的代码。

    接下来看看如何使用tree-shaking?

    其实从webpack2开始就已经开始支持tree-shaking的特性了,webpack2正式内置支持2015模块,和未引用模块的检测能力。新的webpack4正式版扩展了这个检测能力。通过package.json的sideEffects属性标记。向complier提供提示,表面项目中的哪些文件是pure(纯es2015模块)由此,可以安全的删除文件中未使用的部分。如果使用的webpack4只需要将webpack4设置为production,即可开启tree-shaking.如果使用的是webpack2可能你会发现tree-shaking并不起作用,因为bable会将代码编译成Commonjs模块。而tree-shaking不支持commonjs,所以需要配置不转意:

    options: {
        presets: [
            ['es2015', {modules: false}]
        ]
    }

    sideEffect的一些说明:

    sideEffect是指哪些当import的时候会执行的一些动作。但是不一定会有任何export,比如ployfill,polyfill不对外暴露方法给主程序使用。tree-shaking不能自动识别哪些代码属于side effect.因此手动指定这些代码显的非常重要。如果不指定可能会出现一些意想不到的问题。

    在webpack中是通过package.json的sideEffect属性来实现。

    {
        "name": "tree-shaking",
        "sideEffect": false
    }

    如果所有的代码都不包括副作用,我们就可以简单的将该属性标记为false来告诉webpack,它可以安全的删除用不到的export导出。

    如果代码确实有一些副作用:那么可以提供一个数组:

    {
        "name": "tree-shaking",
        "sideEffect": ["./src/common/polyfill.js"]
    }

    总的来说:

    tree-shaking不支持动态导入,只支持纯静态导入;

    webpack中可以在项目package.json文件中,添加一个sideEffect属性,用于手动指定副作用的脚本。

    展开全文
  • 一、什么是 Tree ShakingTree-Shaking 是一种基于 ES Module 规范的 Dead Code Elimination 技术,它会在运行过程中静态分析模块之间的导...

    一、什么是 Tree Shaking

    Tree-Shaking 是一种基于 ES Module 规范的 Dead Code Elimination 技术,它会在运行过程中静态分析模块之间的导入导出,确定 ESM 模块中哪些导出值未曾其它模块使用,并将其删除,以此实现打包产物的优化。

    Tree Shaking 较早前由 Rich Harris 在 Rollup 中率先实现,Webpack 自 2.0 版本开始接入,至今已经成为一种应用广泛的性能优化手段。

    1.1 在 Webpack 中启动 Tree Shaking

    在 Webpack 中,启动 Tree Shaking 功能必须同时满足三个条件:

    • 使用 ESM 规范编写模块代码

    • 配置 optimization.usedExportstrue,启动标记功能

    • 启动代码优化功能,可以通过如下方式实现:

      • 配置 mode = production

      • 配置 optimization.minimize = true

      • 提供 optimization.minimizer 数组

    例如:

    // webpack.config.js
    module.exports = {
      entry: "./src/index",
      mode: "production",
      devtool: false,
      optimization: {
        usedExports: true,
      },
    };
    

    1.2 理论基础

    在 CommonJs、AMD、CMD 等旧版本的 JavaScript 模块化方案中,导入导出行为是高度动态,难以预测的,例如:

    if(process.env.NODE_ENV === 'development'){
      require('./bar');
      exports.foo = 'foo';
    }
    

    而 ESM 方案则从规范层面规避这一行为,它要求所有的导入导出语句只能出现在模块顶层,且导入导出的模块名必须为字符串常量,这意味着下述代码在 ESM 方案下是非法的:

    if(process.env.NODE_ENV === 'development'){
      import bar from 'bar';
      export const foo = 'foo';
    }
    

    所以,ESM 下模块之间的依赖关系是高度确定的,与运行状态无关,编译工具只需要对 ESM 模块做静态分析,就可以从代码字面量中推断出哪些模块值未曾被其它模块使用,这是实现 Tree Shaking 技术的必要条件。

    1.3 示例

    对于下述代码:

    // index.js
    import {bar} from './bar';
    console.log(bar);
    
    // bar.js
    export const bar = 'bar';
    export const foo = 'foo';
    

    示例中,bar.js 模块导出了 barfoo ,但只有 bar 导出值被其它模块使用,经过 Tree Shaking 处理后,foo 变量会被视作无用代码删除。

    二、实现原理

    Webpack 中,Tree-shaking 的实现一是先「标记」出模块导出值中哪些没有被用过,二是使用 Terser 删掉这些没被用到的导出语句。标记过程大致可划分为三个步骤:

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

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

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

    标记功能需要配置 optimization.usedExports = true 开启

    也就是说,标记的效果就是删除没有被其它模块使用的导出语句,比如:

    示例中,bar.js 模块(左二)导出了两个变量:barfoo,其中 foo 没有被其它模块用到,所以经过标记后,构建产物(右一)中 foo 变量对应的导出语句就被删除了。作为对比,如果没有启动标记功能(optimization.usedExports = false 时),则变量无论有没有被用到都会保留导出语句,如上图右二的产物代码所示。

    注意,这个时候 foo 变量对应的代码 const foo='foo' 都还保留完整,这是因为标记功能只会影响到模块的导出语句,真正执行“「Shaking」”操作的是 Terser 插件。例如在上例中 foo 变量经过标记后,已经变成一段 Dead Code —— 不可能被执行到的代码,这个时候只需要用 Terser 提供的 DCE 功能就可以删除这一段定义语句,以此实现完整的 Tree Shaking 效果。

    接下来我会展开标记过程的源码,详细讲解 Webpack 5 中 Tree Shaking 的实现过程,对源码不感兴趣的同学可以直接跳到下一章。

    2.1 收集模块导出

    首先,Webpack 需要弄清楚每个模块分别有什么导出值,这一过程发生在 make 阶段,大体流程:

    关于 Mak e 阶段的更多说明,请参考前文 [万字总结] 一文吃透 Webpack 核心原理
    1. 将模块的所有 ESM 导出语句转换为 Dependency 对象,并记录到 module 对象的 dependencies 集合,转换规则:

    • 具名导出转换为 HarmonyExportSpecifierDependency 对象

    • default 导出转换为 HarmonyExportExpressionDependency 对象

    例如对于下面的模块:

    export const bar = 'bar';
    export const foo = 'foo';
    
    export default 'foo-bar'
    

    对应的dependencies 值为:

    1. 所有模块都编译完毕后,触发 compilation.hooks.finishModules 钩子,开始执行 FlagDependencyExportsPlugin 插件回调

    2. FlagDependencyExportsPlugin 插件从 entry 开始读取 ModuleGraph 中存储的模块信息,遍历所有 module 对象

    3. 遍历 module 对象的 dependencies 数组,找到所有 HarmonyExportXXXDependency 类型的依赖对象,将其转换为 ExportInfo 对象并记录到 ModuleGraph 体系中

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

    参考资料:
    1. [万字总结] 一文吃透 Webpack 核心原理

    2. 有点难的 webpack 知识点:Dependency Graph 深度解析

    2.2 标记模块导出

    模块导出信息收集完毕后,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 属性,记录该导出被如何使用

    7. 结束

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

    2.3 生成代码

    经过前面的收集与标记步骤后,Webpack 已经在 ModuleGraph 体系中清楚地记录了每个模块都导出了哪些值,每个导出值又没那块模块所使用。接下来,Webpack 会根据导出值的使用情况生成不同的代码,例如:

    重点关注 bar.js 文件,同样是导出值,barindex.js 模块使用因此对应生成了 __webpack_require__.d 调用 "bar": ()=>(/* binding */ bar),作为对比 foo 则仅仅保留了定义语句,没有在 chunk 中生成对应的 export。

    关于 W ebpack 产物的内容及 __webpack_require__.d 方法的含义,可参考 Webpack 原理系列六:彻底理解 Webpack 运行时 一文。

    这一段生成逻辑均由导出语句对应的 HarmonyExportXXXDependency 类实现,大体的流程:

    1. 打包阶段,调用 HarmonyExportXXXDependency.Template.apply 方法生成代码

    2. apply 方法内,读取 ModuleGraph 中存储的 exportsInfo 信息,判断哪些导出值被使用,哪些未被使用

    3. 对已经被使用及未被使用的导出值,分别创建对应的 HarmonyExportInitFragment 对象,保存到 initFragments 数组

    4. 遍历 initFragments 数组,生成最终结果

    基本上,这一步的逻辑就是用前面收集好的 exportsInfo 对象未模块的导出值分别生成导出语句。

    2.4 删除 Dead Code

    经过前面几步操作之后,模块导出列表中未被使用的值都不会定义在 __webpack_exports__ 对象中,形成一段不可能被执行的 Dead Code 效果,如上例中的 foo 变量:

    在此之后,将由 Terser、UglifyJS 等 DCE 工具“摇”掉这部分无效代码,构成完整的 Tree Shaking 操作。

    2.5 总结

    综上所述,Webpack 中 Tree Shaking 的实现分为如下步骤:

    • FlagDependencyExportsPlugin 插件中根据模块的 dependencies 列表收集模块导出值,并记录到 ModuleGraph 体系的 exportsInfo

    • FlagDependencyUsagePlugin 插件中收集模块的导出值的使用情况,并记录到 exportInfo._usedInRuntime 集合中

    • HarmonyExportXXXDependency.Template.apply 方法中根据导出值的使用情况生成不同的导出语句

    • 使用 DCE 工具删除 Dead Code,实现完整的树摇效果

    上述实现原理对背景知识要求较高,建议读者同步配合以下文档食用:

    1. [万字总结] 一文吃透 Webpack 核心原理

    2. 有点难的 webpack 知识点:Dependency Graph 深度解析

    3. Webpack 原理系列六:彻底理解 Webpack 运行时

    三、最佳实践

    虽然 Webpack 自 2.x 开始就原生支持 Tree Shaking 功能,但受限于 JS 的动态特性与模块的复杂性,直至最新的 5.0 版本依然没有解决许多代码副作用带来的问题,使得优化效果并不如 Tree Shaking 原本设想的那么完美,所以需要使用者有意识地优化代码结构,或使用一些补丁技术帮助 Webpack 更精确地检测无效代码,完成 Tree Shaking 操作。

    3.1 避免无意义的赋值

    使用 Webpack 时,需要有意识规避一些不必要的赋值操作,观察下面这段示例代码:

    示例中,index.js 模块引用了 bar.js 模块的 foo 并赋值给 f 变量,但后续并没有继续用到 foof 变量,这种场景下 bar.js 模块导出的 foo 值实际上并没有被使用,理应被删除,但 Webpack 的 Tree Shaking 操作并没有生效,产物中依然保留 foo 导出:

    造成这一结果,浅层原因是 Webpack 的 Tree Shaking 逻辑停留在代码静态分析层面,只是浅显地判断:

    • 模块导出变量是否被其它模块引用

    • 引用模块的主体代码中有没有出现这个变量

    没有进一步,从语义上分析模块导出值是不是真的被有效使用。

    更深层次的原因则是 JavaScript 的赋值语句并不「纯」,视具体场景有可能产生意料之外的副作用,例如:

    import { bar, foo } from "./bar";
    
    let count = 0;
    
    const mock = {}
    
    Object.defineProperty(mock, 'f', {
        set(v) {
            mock._f = v;
            count += 1;
        }
    })
    
    mock.f = foo;
    
    console.log(count);
    

    示例中,对 mock 对象施加的 Object.defineProperty 调用,导致 mock.f = foo 赋值语句对 count 变量产生了副作用,这种场景下即使用复杂的动态语义分析也很难在确保正确副作用的前提下,完美地 Shaking 掉所有无用的代码枝叶。

    因此,在使用 Webpack 时开发者需要有意识地规避这些无意义的重复赋值操作。

    3.3 使用 #pure 标注纯函数调用

    与赋值语句类似,JavaScript 中的函数调用语句也可能产生副作用,因此默认情况下 Webpack 并不会对函数调用做 Tree Shaking 操作。不过,开发者可以在调用语句前添加 /*#__PURE__*/ 备注,明确告诉 Webpack 该次函数调用并不会对上下文环境产生副作用,例如:

    示例中,foo('be retained') 调用没有带上 /*#__PURE__*/ 备注,代码被保留;作为对比,foo('be removed') 带上 Pure 声明后则被 Tree Shaking 删除。

    3.3 禁止 Babel 转译模块导入导出语句

    Babel 是一个非常流行的 JavaScript 代码转换器,它能够将高版本的 JS 代码等价转译为兼容性更佳的低版本代码,使得前端开发者能够使用最新的语言特性开发出兼容旧版本浏览器的代码。

    但 Babel 提供的部分功能特性会致使 Tree Shaking 功能失效,例如 Babel 可以将 import/export 风格的 ESM 语句等价转译为 CommonJS 风格的模块化语句,但该功能却导致 Webpack 无法对转译后的模块导入导出内容做静态分析,示例:

    示例使用 babel-loader 处理 *.js 文件,并设置 Babel 配置项 modules = 'commonjs',将模块化方案从 ESM 转译到 CommonJS,导致转译代码(右图上一)没有正确标记出未被使用的导出值 foo。作为对比,右图 2 为 modules = false 时打包的结果,此时 foo 变量被正确标记为 Dead Code。

    所以,在 Webpack 中使用 babel-loader 时,建议将 babel-preset-envmoduels 配置项设置为 false,关闭模块导入导出语句的转译。

    3.4 优化导出值的粒度

    Tree Shaking 逻辑作用在 ESM 的 export 语句上,因此对于下面这种导出场景:

    export default {
        bar: 'bar',
        foo: 'foo'
    }
    

    即使实际上只用到 default 导出值的其中一个属性,整个 default 对象依然会被完整保留。所以实际开发中,应该尽量保持导出值颗粒度和原子性,上例代码的优化版本:

    const bar = 'bar'
    const foo = 'foo'
    
    export {
        bar,
        foo
    }
    

    3.5 使用支持 Tree Shaking 的包

    如果可以的话,应尽量使用支持 Tree Shaking 的 npm 包,例如:

    • 使用 lodash-es 替代 lodash ,或者使用 babel-plugin-lodash 实现类似效果

    不过,并不是所有 npm 包都存在 Tree Shaking 的空间,诸如 React、Vue2 一类的框架原本已经对生产版本做了足够极致的优化,此时业务代码需要整个代码包提供的完整功能,基本上不太需要进行 Tree Shaking。

    展开全文
  • 一、什么是 Tree ShakingTree-Shaking 是一种基于 ES Module 规范的 Dead Code Elimination 技术,它会在运行过程中静态分析模块之间的导...
  • 概念:1 个模块可能有多个⽅法,只要其中的某个⽅法使⽤到了,则整个⽂件都会被打到 bundle ⾥⾯去,tree shaking 就是只把⽤到的⽅法打⼊ bundle ,没⽤到的⽅法会在 uglify 阶段被擦除掉。 使⽤: webpack 默认...
  • Webpack4: Tree-shaking 深度解析

    千次阅读 2019-02-15 04:36:25
    什么是Tree-shaking 所谓Tree-shaking就是‘摇’的意思,作用是把项目中没必要的模块全部抖掉,用于在不同的模块之间消除无用的代码,...至于为什么不完备,可以看一下百度外卖的Tree-shaking原理 Tree-shading原理...
  • Tree-Shaking性能优化实践 - 原理

    千次阅读 2020-07-02 22:38:29
    上图形象的解释了Tree-shaking 的本意,本文所说的前端中的tree-shaking可以理解为通过工具"摇"我们的JS文件,将其中用不到的代码"摇"掉,是一个性能优化的范畴。具体来说,在 webpack 项目中,有一个入口文件,相当...
  • 什么是tree-shaking webpack 2 的到来带来的最棒的新特性之一就是tree-shakingtree-shaking源自于rollup.js,先如今,webpack 2也有类似的做法。 webpack 里的tree-shaking的到来不得不归功于es6规范的模块。为...
  • Tree Shaking概念详解

    千次阅读 2019-07-16 14:20:03
    Tree Shaking指的就是当我引入一个模块的时候,我不引入这个模块的所有代码,我只引入我需要的代码,这就需要借助webpack里面自带的Tree Shaking这个功能,帮助我们实现。Tree Shaking只支持ES Module(import....) ...
  • webpack5的tree shaking值得了解

    千次阅读 2021-01-10 21:43:26
    什么是 tree shakingtree shaking 的意思是,webpack 在打包的时候将会剔除掉被没有被使用到的代码达到减小报体积,缩短 http 请求时间,起到一定效果的页面优化。 那么我们如何使用 tree shaking 呢?这个功能...
  • Tree Shaking只支持ES模块的使用,不支持require这种动态引入模块的方式。 前端中的tree-shaking可以理解为通过工具"摇"我们的JS文件,将其中用不到的代码"摇"掉,是一个性能优化的范畴。具体来说,在 webpack 项目...
  • Vue 3 | 万物皆可 TreeShaking

    千次阅读 2020-07-21 18:20:41
    关于 TreeShaking 这件事情,官方已经强调无数次了,我也在之前的文章中有提到,原因就是 Vue 团队希望能帮助开发者减小 Web 应用的体积。什么是 TreeShakingT...
  • Vue 3.0中的Treeshaking特性?举例说明一下? 1:目的:     目的是Vue团队希望帮助开发者减小web应用体积 2:什么是Treeshaking    TreeShaking是一种术语,在打包时将不打包没有用到的代码,这些代码会成为...
  • 如果程序是一棵树。绿色表示实际用到的源码和 ...通过tree-shaking,就能将没有使用的模块摇掉,这样达到了删除无用代码的目的。 Webpack4默认的production下是会进行tree-shaking的 mode: "development", // .....
  • 1分钟了解 Tree-shaking

    2020-11-24 10:07:10
    Tree-shaking 中文译为摇晃之后的树,用在 webpack 打包中。其功能是移除 JavaScript 上下文中未引用的代码。比如在项目中引用了一个模块 antd 。但其实只使用到了 Button 组件、Input 组件、Form 组件等。那么 ...
  • Webpack 中的 TreeShaking 是什么?

    千次阅读 2021-01-13 06:51:27
    tree shaking就是通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的静态结构特性,例如import和export。 原理 ESM import 只能作为模块顶层的语句出现 import 的模块名...
  • webpack 中的 tree shaking

    2020-10-31 08:44:44
    webpack 中 tree shaking 的用途和原理是什么? 参考链接: https://webpack.docschina.org/guides/tree-shaking/ https://juejin.im/post/6844903544756109319 tree shaking 的基本概念 对无用代码进行剔除,以...
  • 今日文章由“相学长”授权分享,正文从下面开始~,原文地址:http://suo.im/6nDqfr本文将探讨tree-shaking在当下的现状,以及研究为什么tree-shaking依...
  • 本文作者“相学长”原文地址:http://suo.im/6nDqfr本文将探讨tree-shaking在当下的现状,以及研究为什么tree-shaking依旧举步维艰的原因,最终总结当下...
  • esmodule的使用,看这篇文章就好了:... webpack中实现tree-shaking原理 基于UglifyJS 和es-module的特性,将无用的代码干掉 具体原理可以看这篇: https://juejin.cn/post/6955383260759195678
  • 上一篇文章 Tree-Shaking性能优化实践 - 原理篇 介绍了 tree-shaking原理,本文主要介绍 tree-shaking 的实践三. tree-shaking实践webpack2 发布,宣布支持tree-shaking,webpack 3发布,支持作用域提升,生成的...
  • 流程图.png 让我们从源码出发根据 tree-shaking 的核心原理详细地描述一下具体流程: rollup()阶段,解析源码,生成 AST tree,对 AST tree 上的每个节点进行遍历,判断出是否 include(标记避免重复打包),是的话...
  • 天下武功,唯快不破!最新版的 antd 以及 vue 都对 Tree Shaking 提供了支持。我们内部的组件在支持这部分功能时,也专门梳理了相关的特性。这是四月份写的文章了,长时间不...
  • 面试官:那应该就是介绍一下tree shaking及其工作原理? 我:为什么? 面试官:是因为最近面了好多同学,大家都说熟悉webpack,在项目中如何去使用、如何去优化,也都或多或少会提到tree shaking,但是每当我深入去...
  • 目录Tree-Shaking原理副作用成也Babel,败也Babel不够屌的UglifyJS那到底该怎么办?如果是使用webpack打包JavaScript库使用rollup打包JavaScript库使用webpack打包工程化项目总结 本文将探讨tree-shaking在当下的...
  • Tree-Shaking Checklist 既然您已经了解了捆绑和 tree-shaking 工作原理的来龙去脉,让我们为自己画一个清单,您可以在重新访问当前的实现和代码库时将其打印在方便的地方。希望这将节省您的时间,并让您不仅可以...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,194
精华内容 877
关键字:

shaking原理tree