The purpose of Tree shaking

Apps tend to have an entry file, which is like the trunk of a tree, and an entry file has many dependent modules, which are like branches. In reality, you rely on a module but only use some of its functionality. Tree shaking removes useless code by shaking off unused modules.

The module

CommonJS require modules. Exports,exports

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

module.exports = xx
Copy the code

ES2015(ES6) module import,export

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

// main.js
import { foo } from './lib.js';
foo();
Copy the code

Tree Shaking

Tree Shaking is an example of Tree shaking performance optimization practices.

The essence of Tree Shaking is to eliminate useless JavaScript code. Because of the ES6 module, ES6 module dependencies are fixed, independent of the state of the runtime, and can be reliably analyzed statically. This is the foundation for Tree Shaking.Copy the code

Tree-shaking support tools

  • Webpack/UglifyJS
  • rollup
  • Google closure compiler

Today, let’s take a look at what Webpack’s Tree Shaking does

Webpack Tree shaking

What does Tree Shaking do?

Starting with ES6 top-level modules, it is possible to remove unused modules

Look at the code from the example on the official website:

//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;
}
Copy the code

Result: Square’s code was removed

function(e, t, r) {
  "use strict";
  r.r(t), console.log("cube")}Copy the code

2.Webpack Tree shaking refactoring multi-layer modules to extract the code and simplify the function call structure

code

//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'
}
Copy the code

Result: The simplified code is as follows

// Extract the core codefunction(e, t, r) {
  "use strict";
  r.r(t), console.log("entry1")}Copy the code

3.Webpack Tree shaking does not clean IIFE

What is IIFE? IIFE in MDN

code

//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;
}
Copy the code

Result: Square and cude both exist

function(e, t, n) {
  "use strict";
  n.r(t);
  console.log("square");
  console.log(function(e) {
    return console.log("cube"), e * e * e
  }(2))
}
Copy the code

The question here would be why wouldn’t IIFE be cleared? There is no egg Shaking in your tree-shaking analysis. There is a good example. See below

The reason is simple: because IIFE is special, it will be executed when translated (JS is not a compiled language), Webpack does not do flow analysis, it does not know what special things IIFE does, so it will not delete this part of the code like:

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())
Copy the code

result:

Output V6, not V8Copy the code

If V6 this IIFE inside again make some global variable declaration, then of course cannot delete.

4.Webpack Tree shaking For IIFE return functions, if not used, will be cleaned

Of course, Webpack is not so stupid that it can still be removed if the IIFE return function is found to have no place to call it

code

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

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

export function cube(x) {
  console.log('cube');
  return x * x * x;
}
Copy the code

The result is as follows

function(e, t, n) {
  "use strict";
  n.r(t);
  console.log("square"); <= square This internal IIFE code is still in console.log(function(e) {
    return console.log("cube"E * e * e <= square this IIFEreturnMethod is removed because getSquare is not called}(2)}Copy the code

5.Webpack Tree shaking with third-party packages

code

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

//utils.js
import _ from 'lodash'; <= Different references here can result in different bundle resultsexport function getLast(string) {
  console.log('getLast');
  return _.last(string);
}
Copy the code

The result is as follows

import _ from 'lodash'; Js 70.5kib import {last} from'lodash'; Asset Size bundle.js 70.5kib import last from'lodash/last'; <= This reference significantly reduces the Size of the bundleCopy the code

What Webpack Tree Shaking can’t do

80% reduction in volume! As mentioned in unlocking the real potential of Webpack Tree shaking, Webpack Tree shaking is powerful, but still has its drawbacks

code

//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
}
Copy the code

Result: Code that should not be imported

The 'array' function is not used, but parts of the lodash-es package will still be built into bundle.jsCopy the code

This can be solved using the webpack-deep-scope-analysis-plugin

summary

For better use of Webpack Tree shaking, see:

  • Use ES2015(ES6) modules
  • Avoid IIFE
  • If you are using a third-party module, try to use it directly from the file path (this is not the best way).
import { fn } from 'module'; 
=> 
import fn from 'module/XX';
Copy the code

Problems with Babel 1- Syntax conversion (Babel6)

None of the above examples are handled with Babel, but we understand that Babel is still necessary for us in a real project. So what are the problems with using Babel? (The following discussion is based on Babel6)

Let’s look at the code:

//App.js
import { Apple } from './components'Const appleModel = new Apple({<== ='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'
};
Copy the code

The result is as follows

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)} // Only Apple classes, no Person classes (Tree shaking success) // Class is class, no syntax transformation (Babel not shaking success)Copy the code

But what about processing with Babel(babel-loader)?

// app.js and component.js remain unchanged //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'
};
Copy the code

The result is as follows

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" inr && (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() {
        returnThis.model}}]), e}()} // Not only is the Apple class present, but the Person class is also presentCopy the code

Conclusion: Webpack’s Tree Shaking has the ability to remove code blocks that are exported but not used, but when used in conjunction with Babel(6) it becomes problematic

So let’s see what Babel does, this is code that Babel6 processes, right

'use strict';
Object.defineProperty(exports, "__esModule", {
  value: true}); //_createClass is essentially an IIFEfunction() {
  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);
    returnConstructor; }; } ();function _classCallCheck(instance, Constructor) {
  if(! (instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); }} //Person is an 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, [{<== = here another IIFE key is called:'getName',
    value: function getName() {
      returnthis.name; }}]);returnPerson; } ();Copy the code

From the beginning, it is clear that Webpack Tree shaking does not deal with IIFE, so there is code for the Person class in the bundle even if the Person class is not called.

We could set loose: true to make Babel use loose mode when converting, but that would only remove _createClass, and the Person itself would still exist

//webpack.config.js
{
  loader: 'babel-loader',
  options: {
    presets: [["env", { loose: true}}}]]Copy the code

The result is as follows

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
  }()
}
Copy the code

The discussion of Babel6

Class Declaration in IIFE Considered as Side effect See: github.com/mishoo/Ugli…

Conclusion:

  • Uglify doesn’t perform program flow analysis. But rollup did.
  • Variable assignment could cause an side effect
  • Add some /*#__PURE__*/Annotation could help with it/*#__PURE__*/To declare a side-effect free function, so that Webpack can filter out this part of the code during parsing.

About the third point: add /*#__PURE__*/, which is also the execution behavior of Babel7, which is the code handled by Babel7

var Person =
  /*#__PURE__*/ <= Comments are added here
  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() {
        returnthis.name; }}]);returnPerson; } (); exports.Person = Person;Copy the code

So, with Babel7 running, it’s possible to filter out the unused Person class through Webpack.

Problems with Babel 2- Module conversion (Babel6/7)

We already know that CommonJS modules are not the same as ES6 modules. Babel transforms all modules into exports and require by default. We also know that Webpack is Tree shaking based on ES6 modules. So when we use Babel, we should turn this behavior off as follows:

//babel.rc
presets: [["env", 
  { module: false}]]Copy the code

But there is a question: when should we turn this transformation off?

If we’re all in the same App, closing this Module doesn’t make sense, because if it’s closed, the packaged bundle won’t run in the browser (import is not supported). So here we should set it when the App depends on a library package. For example, libraries such as Lodash/LoDash-es, Redux, React-Redux, and Styled – Component exist in both ES5 and ES6 versions

- redux
  - dist
  - es
  - lib
  - src
  ...
Copy the code

Json to enable Webpack to read ES6 files first eg: Redux ES entry

//package.json
"main": "lib/redux.js"."unpkg": "dist/redux.js"."module": "es/redux.js"."typings": "./index.d.ts".Copy the code

Webpack Tree shaking – Side Effect

There is a sideEffects tag mentioned in the official documentation, but there is little detail on what this tag does, and even when running the official example, it is not available in the latest version of Webpack, so there is more confusion about its use. How to use sideEffects after reading Webpack? What does the general meeting do? How does it work? With basic knowledge, we can dig deeper

What does Tree Shaking do

Not:

//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";
Copy the code

Result: Only the code of A is left

function(e, t, r) {
  "use strict";
  r.r(t);
  console.log("a")}Copy the code

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";
Copy the code

Result: The code of A is left, and the code of B also exists

function(e, n, t) {
  "use strict";
  t.r(n);
  console.log("fun"), window.name = "name";
  console.log("a")}Copy the code

Demo3: Add the sideEffects tag

//package.json
{
  "sideEffects": false,}Copy the code

Result: Only the code of A is left, and all the code of side effects in module B is deleted

function(e, t, r) {
  "use strict";
  r.r(t);
  console.log("a")}Copy the code

To sum up: see @asdFasdfads in What Does Webpack 4 Expect From A Package With sideEffects: False

In fact:

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"Translated into:"No side effects."This phrase can be interpreted as"Don't interact with anything outside of the top-level module.".Copy the code

In Demo3, we added “sideEffects”: false, which means:

1. In module B there is some code for side effects (IIFE and operations to change global variables/attributes), but we do not consider it risky to remove it

2. The module has been referenced (imported or re-exported by another module)

Situation A // B.s (function fun() {
  console.log('fun');
})()
window.name = 'name'
export var b = "b";

//index.js
import { b } from "./b"; Analysis: once a module is imported, the code in it will be translated.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 caseThat one of those exports creates a side-effect with another. Once a module is re-exported, according to the ECMA module specification, every time a module re-exports all exports (whether used or unused), C //b.js (C //b.js (function fun() {
  console.log('fun');
})()
window.name = 'name'
export var b = "b"; //index.js // No import and no importexportAnalysis: useless certainly have no what effectCopy the code

As long as the above two points are met, we can safely add this tag to inform Webpack that it is safe to remove unwanted code. Of course, if your code does have some side effects, you can provide an array instead:

"sideEffects": [
    "./src/some-side-effectful-file.js"
]
Copy the code

Conclusion:

If you want to take advantage of Webpack’s Tree Shaking, you need to make some changes to your project. Advice:

1. Third-party libraries:

  • Maintenance of the team: plus as appropriatesideEffectsFlag, and change the Babel configuration to exportES6 module
  • Third-party: try to use versions that provide ES modules

2. The tool:

  • Upgrade Webpack to 4.x
  • Upgrade Babel to 7.x

reference

  • Your tree-shaking is useless: juejin.cn/post/684490…
  • Tree-Shaking Performance Optimization Practices – Principles: juejin.cn/post/684490…
  • Tree-Shaking Performance Optimization Practices: juejin.cn/post/684490…
  • Segmentfault.com/a/119000001…
  • Use the Tree – Shaking: www.xbhub.com/wiki/webpac…
  • Volume reduced by 80%! Unleashing the true potential of Webpack Tree-shaking: juejin.cn/post/684490…
  • Webpack how to eliminate dead code: through the analysis of the scope vincentdchan. Making. IO / 2018/05 / bet… (github.com/vincentdcha…).
  • Did you upgrade Webpack2 today? www.aliued.com/?p=4060
  • Webpack Chinese website: www.webpackjs.com/guides/prod…
  • In the WebpacksideEffectsHow exactly do you use it?Juejin. Cn/post / 684490…