Tree shaking原理及利用

概念

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

Tree shaking晚期由rollup实现,前期webpack2也减少了Tree shaking的性能,在webpack中,Tree shaking指的就是按需加载,即没有被援用的模块不会被打包进来,缩小咱们的包大小,放大利用的加载工夫,出现给用户更佳的体验。

原理

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

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.jsexport function foo() {    return 'foo';}export function bar() {    return 'bar';}
// main.jsimport {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技术还不是很成熟,在解决冗余代码时会往往会因为副作用而不能很好的实现代码的精简,在日常的代码编写中还是须要咱们缩小冗余,晋升代码品质。