Webpack4: Tree-shaking 深度解析

什么是Tree-shaking

所谓Tree-shaking就是‘摇’的意思,做用是把项目中不必的模块所有抖掉,用于在不一样的模块之间消除无用的代码,可列为性能优化的范畴。javascript

Tree-shaking早期由rollup实现,后来webpack2也实现了Tree-shaking的功能,可是至今还不是很完备。至于为何不完备,能够看一下百度外卖的Tree-shaking原理java

Tree-shading原理

Tree-shaking的本质用于消除项目一些没必要要的代码。早在编译原理中就有提到DCE(dead code eliminnation),做用是消除不可能执行的代码,它的工做是使用编辑器判断出某些代码是不可能执行的,而后清除。webpack

Tree-shaking一样的也是消除项目中没必要要的代码,可是和DCE又有略不相同。能够说是DCE的一种实现,它的主要工做是应用于模块间,在打包过程当中抽出有用的部分,用于完成DCE。git

Tree-shaking是依赖ES6模块静态分析的,ES6 module的特色以下:es6

  1. 只能做为模块顶层的语句出现
  2. import 的模块名只能是字符串常量
  3. import binding 是 immutable的

依赖关系肯定,与运行时无关,静态分析。正式由于ES6 module的这些特色,才让Tree-shaking更加流行。github

主要特色仍是依赖于ES6的静态分析,在编译时肯定模块。若是是require,在运行时肯定模块,那么将没法去分析模块是否可用,只有在编译时分析,才不会影响运行时的状态。web

Webpack4的Tree-shaking

webpack从第2版本就开始支持Tree-shaking的功能,可是至今也并不能实现的那么完美。凡是具备反作用的模块,webpack的Tree-shaking就歇菜了。编程

反作用

反作用在咱们项目中,也一样是频繁的出现。知道函数式编程的朋友都会知道这个名词。所谓模块(这里模块可称为一个函数)具备反作用,就是说这个模块是不纯的。这里能够引入纯函数的概念。浏览器

对于相同的输入就有相同的输出,不依赖外部环境,也不改变外部环境。性能优化

符合上述就能够称为纯函数,不符合就是不纯的,是具备反作用的,是可能对外界形成影响的。

webpack自身的Tree-shaking不能分析反作用的模块。以lodash-es这个模块来举个例子

//test.js
import _ from "lodash-es";

const func1 = function(value){
    return _.isArray(value);
}
const func2 = function(value){
    return value=null;
}

export {
    func1,
    func2,
}
//index.js
import {func2} from './test.js'
func2()
复制代码

上述代码在test.js中引入lodash-es,在func1中使用了loadsh,而且这里不符合纯函数的概念,它是具备反作用的。func2是一个纯函数。

在index.js中只引入了func2,而且使用了func2,可见整个代码的执行是和func1是没有任何关系的。咱们经过生产环境打包一下试试看(Tree-shaking只在生产环境生效)

main.js 91.7KB,可见这个结果是符合咱们的预期的,由于func1函数的反作用,webpack自身的Tree-shaking并无检测到这里有不必的模块。解决办法仍是用的,webpack的插件系统是很强大的。

webpack-deep-scope-plugin

webpack-deep-scope-plugin是一位中国同胞(学生)在Google夏令营,在导师Tobias带领下写的一个webpack插件。(此时慢慢的羡慕)

这个插件主要用于填充webpack自身Tree-shaking的不足,经过做用域分析来消除无用的代码。

插件的原理

这个插件是基于做用域分析的,那么都有什么样的做用域?

// module scope start

// Block

{ // <- scope start
} // <- scope end

// Class

class Foo { // <- scope start

} // <- scope end

// If else

if (true) { // <- scope start
   
} /* <- scope end */ else { // <- scope start
  
} // <- scope end

// For

for (;;) { // <- scope start
} // <- scope end

// Catch

try {

} catch (e) { // <- scope start

} // <- scope end

// Function

function() { // <- scope start
} // <- scope end

// Scope

switch() { // <- scope start
} // <- scope end

// module scope end
复制代码

对于ES6模块来讲,上面做用域只有function和class是能够被导出的,其余的做用域能够称之为function和class的子做用域并不能被导出实际上归属于父做用域的。

插件经过分析代码的做用域,进而获得做用域与做用域之间的关系。

分析做用域

分析代码的做用域的基础是创建作AST(Abstract Syntax Tree)抽象语法树上面的。这个能够经过escope来完成。

拿到解析完的AST抽象语法树,利用图的深度优先遍历找到哪些做用域是能够被使用到的,哪些做用域是不能够被使用到的。从而分析做用域之间的关系和导出变量之间的关系。进而执行模块消除。

插件的不足

JavaScript中仍是有一些代码是不会消去的。

根做用域的引用
import { isNull } from 'lodash-es';

export function scope(...args) {
  return isNull(...args);
}

复制代码

在根做用域引用到的做用域不会被消除。

给变量从新赋值
import _ from "lodash-es";

var func1
func1 = function(value){
    return _.isArray(value);
}
const func2 = function(value){
    return value=null;
}

export {
    func1,
    func2,
}
复制代码

上述代码中先定义了func1,而后又给func1赋值,这样缺乏了数据流分析,一样插件也是不能够的。

纯函数调用

引用做者的例子

import _curry1 from './internal/_curry1';
import curryN from './curryN';
import max from './max';
import pluck from './pluck';

var allPass = /*#__PURE__*/_curry1(function allPass(preds) {
  return curryN(reduce(max, 0, pluck('length', preds)), function () {
    var idx = 0;
    var len = preds.length;
    while (idx < len) {
      if (!preds[idx].apply(this, arguments)) {
        return false;
      }
      idx += 1;
    }
    return true;
  });
});
export default allPass;
复制代码

当一个匿名函数被包在一个函数调用中(IIFE也是如此),那么插件是没法分析的。可是若是加上/*#__PURE__*/注释的话,这个插件会把这个函数调用看成一个独立的域,tree-shaking是能够生效的。

探讨的一些问题

咱们都知道在这个ES6泛滥的时代,ES6的代码在项目中出现已经很普遍。(先不考虑线上环境打包成ES5)。上面提到插件的利用做用域来分析。能导出的做用域只有class和funciton。function的状况在上面已经说过,如今来探讨一下class的状况。

no plugin

当不使用插件的时候,咱们来看一下会不会Tree-shaking,预期是会被Tree-shaking。书写下面这样一段简单的代码。

class Test{
    init(value){
        console.log('test init');
    }
}
export {
    Test,
}
复制代码

咱们发如今没有使用插件的状况下,被Tree-shaking了。预期相同。

no plugin + 反作用

当咱们在不适用插件的状况下,而且引入反作用,观察一下会不会打包,预期是不会打包。书写下面代码。

class Test{
    init(value){
        console.log('test init');
        return _.isArray(value);
    }
}
export {
    Test,
}
复制代码

观察打包结果,main.js 91.7KB,并无被Tree-shaking,符合预期的结果。

plugin + 反作用

当咱们使用插件而且代码中存在反作用的状况下,观察打包状况。因为上面的插件原理的铺垫,咱们预期此次是能够Tree-shaking的。利用上例代码来测试。

咱们观察能够看出main.js 6.78KB,Tree-shaking生效。

plugin + 反作用 + babel

因为用户浏览器对ES6支持度不够的缘由,线上的代码不能全是ES6的,有时候咱们要把ES6的代码打包成ES5的,放到线上环境来执行。利用上例代码来测试。

??? 什么鬼,我没有用到它,为何这么大??? 一串懵逼

成也babel,败也babel

懵逼懵逼,babel成就了线上生产环境,但失去了Tree-shaking优化。咱们来看看怎么回事。

没有反作用的状况

当去除调反作用的时候咱们来打包一下。

没有找到test init Tree-shaking生效。为何呢?咱们使用babel线上工具编译一下源代码。

"use strict";

function _instanceof(left, right) { 
    if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
         return right[Symbol.hasInstance](left); 
        } else {
             return left instanceof right; 
            } 
        }

function _classCallCheck(instance, Constructor) { 
    if (!_instanceof(instance, Constructor)) { 
        throw new TypeError("Cannot call a class as a function"); 
    } 
}

function _defineProperties(target, props) { 
    for (var i = 0; i < props.length; i++) { 
        var descriptor = props[i]; 
        descriptor.enumerable = descriptor.enumerable || false; 
        descriptor.configurable = true; 
        if ("value" in descriptor) 
            descriptor.writable = true; 
        Object.defineProperty(target, descriptor.key, descriptor); 
    } 
}

function _createClass(Constructor, protoProps, staticProps) { 
    if (protoProps) 
        _defineProperties(Constructor.prototype, protoProps); 
    if (staticProps) _defineProperties(Constructor, staticProps); 
    return Constructor; }

var Test =
    /*#__PURE__*/
    function () {
        function Test() {
            _classCallCheck(this, Test);
        }

        _createClass(Test, [{
            key: "init",
            value: function init(value) {
                console.log("test init")
            }
        }]);

        return Test;
    }();
复制代码

上面能够看到最新的babel和webpack有了契合,在Test当即执行函数的地方使用了 /*#__PURE__*/(忘记能够往上看),让下面的IIFE变成可分析的,成功了使用了Tree-shaking。

有反作用的状况下

上面探讨状况的时候就得知有反作用的状况下,不能够被打包的。ES6编译代码以下。

"use strict";

function _instanceof(left, right) { 
    if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
         return right[Symbol.hasInstance](left); 
        } else {
             return left instanceof right; 
            } 
        }

function _classCallCheck(instance, Constructor) { 
    if (!_instanceof(instance, Constructor)) { 
        throw new TypeError("Cannot call a class as a function"); 
    } 
}

function _defineProperties(target, props) { 
    for (var i = 0; i < props.length; i++) { 
        var descriptor = props[i]; 
        descriptor.enumerable = descriptor.enumerable || false; 
        descriptor.configurable = true; 
        if ("value" in descriptor) 
            descriptor.writable = true; 
        Object.defineProperty(target, descriptor.key, descriptor); 
    } 
}

function _createClass(Constructor, protoProps, staticProps) { 
    if (protoProps) 
        _defineProperties(Constructor.prototype, protoProps); 
    if (staticProps) _defineProperties(Constructor, staticProps); 
    return Constructor; }

var Test =
    /*#__PURE__*/
    function () {
        function Test() {
            _classCallCheck(this, Test);
        }

        _createClass(Test, [{
            key: "init",
            value: function init(value) {
                console.log("test init")
                return _.isArray(value);
            }
        }]);

        return Test;
    }();
复制代码

这里虽然bable新版契合了webpack,可是仍是有一些问题。本身也没有找出是哪里除了问题,做者说JavaScript代码仍是有一些是不能够清除的,也许就出现到这里。提供一个做者的插件Demo

babel的解决方案

不管是ES6,仍是ES5,Tree-shaking不能生效的缘由总的归根结底仍是由于代码反作用的问题。可想而知代码的书写规范是多么重要。这里我所想出的解决方案有两种。

1.代码的书写规范,尽可能避免反作用。

书写代码过程当中尽可能使用纯函数的方式来写代码,保持书写规范,不让代码有反作用。例如把class类引用的反作用改为纯的。

class Test{
    init(value,_){  //参数引入lodash模块
        console.log('test init');
        return _.isArray(value);
    }
}
export{
    Test,
}
复制代码

能够看出,没有反作用的ES6代码编译成ES5,Tree-shaking也是生效的。

2.灵活使用ES6代码

两套代码。当浏览器支持的时候,就使用ES6的代码,ES5的代码。此方案可参考浏览器支持ES6的最优解决方案

总结

项目中不免会一些用不到的模块占位置影响咱们的项目,Tree-shaking的出现也为开发者在性能优化方面提供了很是大的帮助,灵活使用Tree-shaking才能让Tree-shaking发挥做用,处理好项目中代码的反作用可使项目更加的完美。

引用文章

webpack 如何经过做用域分析消除无用代码

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

原文发布于Webpack4:Tree-shaking深度解析

相关文章
相关标签/搜索