2

Tree shaking原理及应用

概念

Tree shaking字面意就是“摇树”,通过摇树将树上枯黄的叶子摇落,在项目开发中,我们会按照模块划分的方式将代码组织起来,tree shaking的作用是把项目中没必要的代码全部抖落掉,消除被引用,删除没被调用的无用模块代码,该优化最终实现的是代码体积的减少,也属于项目性能优化的一部分。

Tree shaking早期由rollup实现,后期webpack2也增加了Tree shaking的功能,在webpack中,Tree shaking指的就是按需加载,即没有被引用的模块不会被打包进来,减少我们的包大小,缩小应用的加载时间,呈现给用户更佳的体验。

原理

Tree Shaking在去除代码冗余的过程中,程序会从入口文件出发扫描所有的模块依赖,以及模块的子依赖,然后将它们链接起来形成一个“抽象语法树”(AST)。随后,运行所有代码,查看哪些代码是用到过的,做好标记。最后,再将“抽象语法树”中没有用到的代码“摇落”。经历这样一个过程后,就去除了没有用到的代码。

image-20200729195132323

Tree Shaking实现的关键得益于ES Module模块的静态分析(Static module resolution)功能。那么什么是静态分析呢?

静态分析

在ES Module中,我们可以将模块的加载分为两个阶段:静态分析编译执行

<img src="https://tva1.sinaimg.cn/large/007S8ZIlly1gjnjerytkuj30fi0c8gm5.jpg" alt="image-20201013111828842" style="zoom:50%;" />

所谓静态分析,即在代码执行前就能对整体代码依赖调用关系等进行分析读取;

下面我们看下ES Module的特性:

  1. 只能作为模块顶层的语句出现(而不嵌套在条件语句中)
  2. import 的模块名只能是字符串常量(只对文件进行字符串读取)
  3. 导入和导出语句没有动态部分(不允许使用变量等)

ES6中,实现了完全静态的导入语法:import,这也意味着下面的导入是不可行的:

// 报错,不可嵌套在条件语句中
// 静态分析阶段代码未编译执行,拿不到该变量
if(condition) {
    import foo from "foo";
} else {
    import bar from "bar";
}

// 报错,不可使用表达式
import {'f'+'oo'} from 'module1';

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

import foo from "foo";
import bar from "bar";

if(condition) {
    // foo.xxxx
} else {
    // bar.xxx
}

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

不同于ES ModuleCommonJS支持动态加载模块,在加载前是无法确定模块是否有被调用,所以并不支持tree shaking

if(condition) {
    myDynamicModule = require("foo");
} else {
    myDynamicModule = require("bar");
}
ES6 modules这些设计虽然灵活性不如 CommonJS 的 require,但却保证了 ES6 modules 的依赖关系是确定 (deterministic) 的,和运行时的状态无关,从而也就保证了 ES6 modules 是可以进行可靠的静态分析的

我们知道webpack本身是一个插件的集合,那么tree shaking功能又是哪些插件来实现的呢?

目前集成tree shaking功能的插件有以下几种:

  • UglifyJS
  • webpack-rollup-loader
  • Babel Minify Webpack Plugin

应用

下面我们看下官方文档解释:

webpack 2 正式版本内置支持 ES2015 模块(也叫做 harmony modules)和未使用模块检测能力。新的 webpack 4 正式版本扩展了此检测能力,通过 package.json"sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯正 ES2015 模块)",由此可以安全地删除文件中未使用的部分。

配置方式:

1、sideEffects副作用

pakeage.json文件添加sideEffects配置,排除非es module类型模块,避免产生副作用;

如果所有代码都不包含 side effect,我们就可以简单地将该属性标记为 false,来告知 webpack,它可以安全地删除未用到的 export。

sideEffects 更为有效 是因为它允许跳过整个模块/文件和整个文件子树。

{
  "name": "webpack-app",
  "version": "1.0.0",
  "description": "webpack app description",
  "main": "",
  "sideEffects": [
    "*.scss"
  ]
}
2、.babelrc文件

在使用babel编译时,默认会将模块编译成CommonJS,将modules设置为false,避免将ES Module模块类型转换

{
  "presets": [
    [
        "es2015",
        {
            "modules": false
        }
    ]
  ]
}

运行实例

下面是webpack+babel实现tree shaking的例子:

// helpers.js
export function foo() {
    return 'foo';
}
export function bar() {
    return 'bar';
}
// main.js
import {foo} from './helpers';

let elem = document.getElementById('output');
elem.innerHTML = `Output: ${foo()}`;
未配置使用tree shaking:

Babel es2015,中包含插件transform-es2015-modules-commonjs,它的作用是将es2015代码转换成commonjs,这样一来,就失去了静态解析的基本条件

// .babelrc文件
{
    presets: ['es2015'],
}

编译后,输出的helpers模块代码:

function(module, exports) {

    'use strict';

    Object.defineProperty(exports, "__esModule", {
        value: true
    });
    exports.foo = foo;
    exports.bar = bar;
    function foo() {
        return 'foo';
    }
    function bar() {
        return 'bar';
    }

}

如上代码,我们可以看到是同时exports导出了 foo和bar;

配置tree shaking:

编译后,输出的helpers模块代码,只有foo模块被导出了,但是还是存在bar方法:

function(module, exports, __webpack_require__) {    
  /* harmony export */ exports["foo"] = foo;
  /* unused harmony export bar */;
  function foo() {
    return 'foo';
  }
  function bar() {
    return 'bar';
  }
 }

经过代码压缩过后,才真正实现了无用代码的剔除:

function (t, n, r) {
  function e() {
    return "foo"
  }
  n.foo = e
}

总结

通过以上讲解,我们了解到为了实现在项目中应用tree shaking,需要具备以下几个条件:

  • 使用 ES2015 模块语法(即 importexport)。
  • 确保没有编译器将的 ES2015 模块语法转换为 CommonJS 的(顺带一提,这是现在常用的 @babel/preset-env 的默认行为)。
  • 在项目的 package.json 文件中,添加 "sideEffects" 属性。
  • 使用 mode"production" 的配置项以启用更多优化项,包括压缩代码与 tree shaking。

当前tree shaking技术还不是很成熟,在处理冗余代码时会往往会因为副作用而不能很好的实现代码的精简,在日常的代码编写中还是需要我们减少冗余,提升代码质量。


林之夏
9 声望0 粉丝