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 appropriate
sideEffects
Flag, 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 Webpack
sideEffects
How exactly do you use it?Juejin. Cn/post / 684490…