Webpack Tree shaking 深刻探究

Tree shaking的目的

App每每有一个入口文件,至关于一棵树的主干,入口文件有不少依赖的模块,至关于树枝。实际状况中,虽然依赖了某个模块,但其实只使用其中的某些功能。经过Tree shaking,将没有使用的模块摇掉,这样来达到删除无用代码的目的。html

模块

CommonJS的模块require modules.exports,exportsreact

var my_lib;
if (Math.random()) {
    my_lib = require('foo');
} else {
    my_lib = require('bar');
}

module.exports = xx
复制代码

ES2015(ES6)的模块import,exportwebpack

// lib.js
export function foo() {}
export function bar() {}

// main.js
import { foo } from './lib.js';
foo();
复制代码

Tree shaking的原理

关于Tree shaking的原理,在Tree Shaking性能优化实践 - 原理篇已经说的比较清楚,简单来讲。git

Tree shaking的本质是消除无用的JavaScript代码。
由于ES6模块的出现,ES6模块依赖关系是肯定的,`和运行时的状态无关`,能够进行可靠的静态分析,
这就是Tree shaking的基础。
复制代码

支持Tree-shaking的工具

  • Webpack/UglifyJS
  • rollup
  • Google closure compiler

今天,咱们来看一下Webpack的Tree shaking作了什么github

Webpack Tree shaking

Tree shaking到底能作哪些事情??web

1.Webpack Tree shaking从ES6顶层模块开始分析,能够清除未使用的模块

官网的例子来看 代码npm

//App.js
import { cube } from './utils.js';
cube(2);

//utils.js
export function square(x) {
  console.log('square');
  return x * x;
}

export function cube(x) {
  console.log('cube');
  return x * x * x;
}
复制代码

result: square的代码被移除json

function(e, t, r) {
  "use strict";
  r.r(t), console.log("cube")
}
复制代码

2.Webpack Tree shaking会对多层调用的模块进行重构,提取其中的代码,简化函数的调用结构

代码redux

//App.js
import { getEntry } from './utils'
console.log(getEntry());

//utils.js
import entry1 from './entry.js'
export function getEntry() {
  return entry1();
}

//entry.js
export default function entry1() {
  return 'entry1'
}
复制代码

result: 简化后的代码以下segmentfault

//摘录核心代码
function(e, t, r) {
  "use strict";
  r.r(t), console.log("entry1")
}
复制代码

3.Webpack Tree shaking不会清除IIFE(当即调用函数表达式)

IIFE是什么?? IIFE in MDN

代码

//App.js
import { cube } from './utils.js';
console.log(cube(2));

//utils.js
var square = function(x) {
  console.log('square');
}();

export function cube(x) {
  console.log('cube');
  return x * x * x;
}
复制代码

result: square和cude都存在

function(e, t, n) {
  "use strict";
  n.r(t);
  console.log("square");
  console.log(function(e) {
    return console.log("cube"), e * e * e
  }(2))
}
复制代码

这里的问题会是为何不会清除IIFE?在你的Tree-Shaking并没什么卵用中有过度析,里面有一个例子比较好,见下文

缘由很简单:由于IIFE比较特殊,它在被翻译时(JS并不是编译型的语言)就会被执行,Webpack不作程序流分析,它不知道IIFE会作什么特别的事情,因此不会删除这部分代码 好比:

var V8Engine = (function () {
  function V8Engine () {}
  V8Engine.prototype.toString = function () { return 'V8' }
  return V8Engine
}())

var V6Engine = (function () {
  function V6Engine () {}
  V6Engine.prototype = V8Engine.prototype // <---- side effect
  V6Engine.prototype.toString = function () { return 'V6' }
  return V6Engine
}())

console.log(new V8Engine().toString())
复制代码

result:

输出V6,而并非V8
复制代码

若是V6这个IIFE里面再搞一些全局变量的声明,那就固然不能删除了。

4.Webpack Tree shaking对于IIFE的返回函数,若是未使用会被清除

固然Webpack也没有那么的傻,若是发现IIFE的返回函数没有地方调用的话,依旧是能够被删除的

代码

//App.js
import { cube } from './utils.js';
console.log(cube(2));

//utils.js
var square = function(x) {
  console.log('square');
  return x * x;
}();

function getSquare() {
  console.log('getSquare');
  square();
}

export function cube(x) {
  console.log('cube');
  return x * x * x;
}
复制代码

result: 结果以下

function(e, t, n) {
  "use strict";
  n.r(t);
  console.log("square");   <= square这个IIFE内部的代码还在
  console.log(function(e) {
    return console.log("cube"), e * e * e  <= square这个IIFEreturn的方法由于getSquare未被调用而被删除
  }(2))
}
复制代码

5.Webpack Tree shaking结合第三方包使用

代码

//App.js
import { getLast } from './utils.js';
console.log(getLast('abcdefg'));

//utils.js
import _ from 'lodash';   <=这里的引用方式不一样,会形成bundle的不一样结果

export function getLast(string) {
  console.log('getLast');
  return _.last(string);
}
复制代码

result: 结果以下

import _ from 'lodash';
    Asset      Size 
bundle.js  70.5 KiB

import { last } from 'lodash';
    Asset      Size
bundle.js  70.5 KiB

import last from 'lodash/last';   <=这种引用方式明显下降了打包后的大小
    Asset      Size
bundle.js  1.14 KiB
复制代码

Webpack Tree shaking作不到的事情

体积减小80%!释放webpack tree-shaking的真正潜力一文中提到了,Webpack Tree shaking虽然很强大,可是依旧存在缺陷

代码

//App.js
import { Add } from './utils'
Add(1 + 2);

//utils.js
import { isArray } from 'lodash-es';

export function array(array) {
  console.log('isArray');
  return isArray(array);
}

export function Add(a, b) {
  console.log('Add');
  return a + b
}
复制代码

result: 不应导入的代码

这个`array`函数未被使用,可是lodash-es这个包的部分代码仍是会被build到bundle.js中
复制代码

可使用这个插件webpack-deep-scope-analysis-plugin解决

小结

若是要更好的使用Webpack Tree shaking,请知足:

  • 使用ES2015(ES6)的模块
  • 避免使用IIFE
  • 若是使用第三方的模块,能够尝试直接从文件路径引用的方式使用(这并非最佳的方式)
import { fn } from 'module'; 
=> 
import fn from 'module/XX';
复制代码

Babel带来的问题1-语法转换(Babel6)

以上的全部示例都没有使用Babel进行处理,可是咱们明白在真实的项目中,Babel对于咱们仍是必要的。那么若是使用了Babel会带来什么问题呢?(如下讨论创建在Babel6的基础上)

咱们看代码

//App.js
import { Apple } from './components'

const appleModel = new Apple({   <==仅调用了Apple
  model: 'IphoneX'
}).getModel()

console.log(appleModel)

//components.js
export class Person {
  constructor ({ name, age, sex }) {
    this.className = 'Person'
    this.name = name
    this.age = age
    this.sex = sex
  }
  getName () {
    return this.name
  }
}

export class Apple {
  constructor ({ model }) {
    this.className = 'Apple'
    this.model = model
  }
  getModel () {
    return this.model
  }
}

//webpack.config.js
const path = require('path');
module.exports = {
  entry: [
    './App.js'
  ],
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, './build'),
  },
  module: {},
  mode: 'production'
};
复制代码

result: 结果以下

function(e, t, n) {
  "use strict";
  n.r(t);
  const r = new class {
    constructor({ model: e }) {
      this.className = "Apple", this.model = e
    }
    getModel() {
      return this.model
    }
  }({ model: "IphoneX" }).getModel();
  console.log(r)
}

//仅有Apple的类,没有Person的类(Tree shaking成功)
//class仍是class,并无通过语法转换(没有通过Babel的处理)
复制代码

可是若是加上Babel(babel-loader)的处理呢?

//App.js和component.js保持不变
//webpack.config.js
const path = require('path');
module.exports = {
  entry: [
    './App.js'
  ],
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, './buildBabel'),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['env']
          }
        }
      }
    ]
  },
  mode: 'production'
};
复制代码

result:结果以下

function(e, n, t) {
  "use strict";
  Object.defineProperty(n, "__esModule", { value: !0 });
  var r = function() {
    function e(e, n) {
      for(var t = 0; t < n.length; t++) {
        var r = n[t];
        r.enumerable = r.enumerable || !1, r.configurable = !0, "value" in r && (r.writable = !0), Object.defineProperty(e, r.key, r)
      }
    }
    return function(n, t, r) {
      return t && e(n.prototype, t), r && e(n, r), n
    }
  }();
  function o(e, n) {
    if(!(e instanceof n)) throw new TypeError("Cannot call a class as a function")
  }
  n.Person = function() {
    function e(n) {
      var t = n.name, r = n.age, u = n.sex;
      o(this, e), this.className = "Person", this.name = t, this.age = r, this.sex = u
    }
    return r(e, [{
      key: "getName", value: function() {
        return this.name
      }
    }]), e
  }(), n.Apple = function() {
    function e(n) {
      var t = n.model;
      o(this, e), this.className = "Apple", this.model = t
    }
    return r(e, [{
      key: "getModel", value: function() {
        return this.model
      }
    }]), e
  }()
}

//此次不只Apple类在,Person类也存在(Tree shaking失败了)
//class已经被babel处理转换了
复制代码

结论:Webpack的Tree Shaking有能力除去导出但没有使用的代码块,可是结合Babel(6)使用以后就会出现问题

那么咱们看看Babel到底干了什么, 这是被Babel6处理的代码

'use strict';
Object.defineProperty(exports, "__esModule", {
  value: true
});

//_createClass本质上也是一个IIFE
var _createClass = 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);
    }
  }
  return function(Constructor, protoProps, staticProps) {
    if(protoProps) defineProperties(Constructor.prototype, protoProps);
    if(staticProps) defineProperties(Constructor, staticProps);
    return Constructor;
  };
}();

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

//Person本质上也是一个IIFE
var Person = exports.Person = function () {
  function Person(_ref) {
    var name = _ref.name,
        age = _ref.age,
        sex = _ref.sex;
    _classCallCheck(this, Person);
    this.className = 'Person';
    this.name = name;
    this.age = age;
    this.sex = sex;
  }
  _createClass(Person, [{    <==这里调用了另外一个IIFE
    key: 'getName',
    value: function getName() {
      return this.name;
    }
  }]);
  return Person;
}();
复制代码

从最开始,咱们就清楚Webpack Tree shaking是不处理IIFE的,因此这里即便没有调用Person类在bundle中也存在了Person类的代码。

咱们能够设定使用loose: true来使得Babel在转化时使用宽松的模式,可是这样也仅仅只能去除_createClass,Person自己依旧存在

//webpack.config.js
{
  loader: 'babel-loader',
  options: {
    presets: [["env", { loose: true }]]
  }
}
复制代码

result: 结果以下

function(e, t, n) {
  "use strict";
  function r(e, t) {
    if(!(e instanceof t)) throw new TypeError("Cannot call a class as a function")
  }
  t.__esModule = !0;
  t.Person = function() {
    function e(t) {
      var n = t.name, o = t.age, u = t.sex;
      r(this, e), this.className = "Person", this.name = n, this.age = o, this.sex = u
    }
    return e.prototype.getName = function() {
      return this.name
    }, e
  }(), t.Apple = function() {
    function e(t) {
      var n = t.model;
      r(this, e), this.className = "Apple", this.model = n
    }
    return e.prototype.getModel = function() {
      return this.model
    }, e
  }()
}
复制代码

Babel6的讨论

Class declaration in IIFE considered as side effect 详见:github.com/mishoo/Ugli…

总结:

  • Uglify doesn't perform program flow analysis. but rollup did(Uglify不作程序流的分析,可是rollup作了)
  • Variable assignment could cause an side effect(变量的赋值可能会引发反作用)
  • Add some /*#__PURE__*/ annotation could help with it(能够尝试添加注释/*#__PURE__*/的方式来声明一个无反作用的函数,使得Webpack在分析处理时能够过滤掉这部分代码)

关于第三点:添加/*#__PURE__*/,这也是Babel7的执行行为, 这是被Babel7处理的代码

var Person =
  /*#__PURE__*/ <=这里添加了注释
  function() {
    function Person(_ref) {
      var name = _ref.name,
        age = _ref.age,
        sex = _ref.sex;
      _classCallCheck(this, Person);
      this.className = 'Person';
      this.name = name;
      this.age = age;
      this.sex = sex;
    }
    _createClass(Person, [{
      key: "getName",
      value: function getName() {
        return this.name;
      }
    }]);
    return Person;
  }();
exports.Person = Person;
复制代码

因此,在Babel7的运行环境下,通过Webpack的处理是能够过滤掉这个未使用的Person类的。

Babel带来的问题2-模块转换(Babel6/7)

咱们已经清楚,CommonJS模块和ES6的模块是不同的,Babel在处理时默认将全部的模块转换成为了exports结合require的形式,咱们也清楚Webpack是基于ES6的模块才能作到最大程度的Tree shaking的,因此咱们在使用Babel时,应该将Babel的这一行为关闭,方式以下:

//babel.rc
presets: [["env", 
  { module: false }
]]
复制代码

但这里存在一个问题:什么状况下咱们该关闭这个转化?

若是咱们都在一个App中,这个module的关闭是没有意义的,由于若是关闭了,那么打包出来的bundle是没有办法在浏览器里面运行的(不支持import)。因此这里咱们应该在App依赖的某个功能库打包时去设置。 好比:像lodash/lodash-es,redux,react-redux,styled-component这类库都同时存在ES5和ES6的版本

- redux
  - dist
  - es
  - lib
  - src
  ...
复制代码

同时在packages.json中设置入口配置,就可让Webpack优先读取ES6的文件 eg: Redux ES 入口

//package.json
"main": "lib/redux.js",
"unpkg": "dist/redux.js",
"module": "es/redux.js",
"typings": "./index.d.ts",
复制代码

Webpack Tree shaking - Side Effect

在官方文档中提到了一个sideEffects的标记,可是关于这个标记的做用,文档详述甚少,甚至运行官方给了例子,在最新的版本的Webpack中也没法获得它解释的结果,所以对这个标记的用法存在更多的疑惑。读完Webpack中的sideEffects到底该怎么用? 这篇大体会对作了什么?怎么用? 有了基本的认知,咱们能够接着深挖

Tree shaking到底作了什么

Demo1:

//App.js
import { a } from 'tree-shaking-npm-module-demo'
console.log(a);

//index.js
export { a } from "./a";
export { b } from "./b";
export { c } from "./c";

//a.js
export var a = "a";

//b.js
export var b = "b";

//c.js
export var c = "c";
复制代码

result: 仅仅留下了a的代码

function(e, t, r) {
  "use strict";
  r.r(t);
  console.log("a")
}
复制代码

Demo2:

//App.js
import { a } from 'tree-shaking-npm-module-demo'
console.log(a);

//index.js
export { a } from "./a";
export { b } from "./b";
export { c } from "./c";

//a.js
export var a = "a";

//b.js
(function fun() {
  console.log('fun');
})()
window.name = 'name'
export var b = "b";

//c.js
export var c = "c";
复制代码

result: 留下了a的代码,同时还存在b中的代码

function(e, n, t) {
  "use strict";
  t.r(n);
  console.log("fun"), window.name = "name";
  console.log("a")
}
复制代码

Demo3: 添加sideEffects标记

//package.json
{
  "sideEffects": false,
}
复制代码

result: 仅留下了a的代码,b模块中的全部的反作用的代码被删除了

function(e, t, r) {
  "use strict";
  r.r(t);
  console.log("a")
}
复制代码

综上:参考What Does Webpack 4 Expect From A Package With sideEffects: false@asdfasdfads(那个目前只有三个赞)的回答

实际上:

The consensus is that "has no sideEffects" phrase can be decyphered as "doesn't talk to things external to the module at the top level".
译为:
"没有反作用"这个短语能够被解释为"不与顶层模块之外的东西进行交互"复制代码

在Demo3中,咱们添加了"sideEffects": false也就意味着:

1.在b模块中虽然有一些反作用的代码(IIFE和更改全局变量/属性的操做),可是咱们不认为删除它是有风险的

2.模块被引用过(被其余的模块import过或从新export过)

状况A
//b.js
(function fun() {
  console.log('fun');
})()
window.name = 'name'
export var b = "b";

//index.js
import { b } from "./b";   
分析:
b模块一旦被import,那么其中的代码会在翻译时执行

状况B
//b.js
(function fun() {
  console.log('fun');
})()
window.name = 'name'
export var b = "b";

//index.js
export { b } from "./b";
分析:
According to the ECMA Module Spec, whenever a module reexports all exports (regardless if used or unused) need to be evaluated and executed in the case that one of those exports created a side-effect with another.
b模块一旦被从新re-export,根据ECMA模块规范,每当模块从新导出全部导出(不管使用或未使用)时,都须要对其中一个导出与另外一个导出产生反作用的状况进行评估和执行

状况C
//b.js
(function fun() {
  console.log('fun');
})()
window.name = 'name'
export var b = "b";

//index.js
//没有import也没有export
分析:
没用的固然没有什么影响
复制代码

只要知足以上两点:咱们就能够根据状况安全的添加这个标记来通知Webpack能够安全的删除这些无用的代码。 固然若是你的代码确实有一些反作用,那么能够改成提供一个数组:

"sideEffects": [
    "./src/some-side-effectful-file.js"
]
复制代码

总结:

若是想利用好Webpack的Tree shaking,须要对本身的项目进行一些改动。 建议:

1.对第三方的库:

  • 团队的维护的:视状况加上sideEffects标记,同时更改Babel配置来导出ES6模块
  • 第三方的:尽可能使用提供ES模块的版本

2.工具:

  • 升级Webpack到4.x
  • 升级Babel到7.x

参考

相关文章
相关标签/搜索