webpack 等构建工具提供 tree-shaking
机制, 利用 es6
中Module
的语法的 export
和import
语法进行动态剖析, 对无用代码进行剔除, 缩小打包后的代码量.
启动 webpack 的tree-shaking
, 须要:
- webpack 在 v2.0 以上
- 开启代码压缩
webpack 只是标记语句依赖以及是否应用, tree-shaking
的具体实现个别是由压缩器提供实现, 如 webpack 默认的压缩工具 terser-webpack-plug
就反对 tree-shaking
. 本文不是探讨如何启用tree-shaking
, 也不钻研tree shaking
的底层原理, 只通过案例, 钻研 tree-shaking
对代码的一些影响.
演示中的 webpack
的配置为:
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
// filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
devtool: 'hidden-source-map',
};
js 压缩器的选项采纳默认, 在源码中增加非凡的符号
>>>
来查看成果.
模块接口没有相互依赖
模块中的代码:
export var firstName = '>>>Michael';
export var lastName = '>>>Jackson';
export var year = 1958;
export var person = {name: '>>>joyer'};
export function log(info) {console.log(`>>>${info}`);
}
export default function() {console.log('>>>i am default');
}
在入口文件 (src/index.js
) 中, 如果导入 (包含全量导入, 默认导入, 具名导入) 但没有应用的话:
import {firstName} from './mod1.js';
console.log('>>>index.js');
整个模块都会被疏忽, 打包后的要害代码:
[function (e, t, r) {
"use strict";
r.r(t);
console.log(">>>index.js")
}
]
如果应用了导入模块的并应用某一接口:
import {firstName} from './mod1.js';
console.log('>>>in index.js');
console.log(firstName);
打包后后的要害代码为:
[function (e, t, n) {
"use strict";
n.r(t);
console.log(">>>in index.js"), console.log(">>>Michael")
}
]
函数也相似:
import {log} from './mod1.js';
console.log('>>>in index.js');
((() => {log();
})());
打包后的要害代码:
[function (e, t, n) {
"use strict";
n.r(t);
var r;
console.log(">>>in index.js"), console.log(">>>" + r)
}
]
简单的调用状况下:
import {log} from './mod1.js';
console.log('>>>in index.js');
var modLod = log;
var _modLod = modLod();
((() => {function callModLog () {console.log('callModLog==>', callModLog);
_modLod();}
_modLod && _modLod();
callModLog();})());
打包后的要害代码:
[function (e, n, t) {
"use strict";
t.r(n);
console.log(">>>in index.js");
var r = function (e) {console.log(">>>" + e)
}();
r && r(),
function e() {console.log("callModLog==>", e), r()}()}
]
默认导入跟具名导入一样:
import log from './mod1.js';
console.log('>>>i am index.js');
log();
打包后要害代码:
[function (e, t, n) {
"use strict";
n.r(t);
console.log(">>>i am index.js"), console.log(">>>i am default")
}
]
全量导入, 申明式的应用接口, 跟具名导入统一:
import * as Mod from './mod1.js';
((() => {console.log('>>>i am index.js');
console.log(Mod.firstName);
Mod.log();})());
打包后的要害代码:
[function (e, t, n) {
"use strict";
n.r(t);
var r;
console.log(">>>i am index.js"), console.log(">>>Michael"), console.log(">>>" + r)
}
]
如果是动静的应用, 则丢失 tree shaking
成果:
import * as Mod from './mod1.js';
((() => {console.log('>>>i am index.js');
console.log(Mod['firstName']);
let methodName = 'log';
Mod[methodName]();})());
打包后的要害代码:
[function (e, n, t) {
"use strict";
t.r(n);
var r = {};
t.r(r), t.d(r, "firstName", (function () {return o})), t.d(r, "lastName", (function () {return u})), t.d(r, "year", (function () {return i})), t.d(r, "person", (function () {return l})), t.d(r, "log", (function () {return c})), t.d(r, "default", (function () {return f}));
var o = ">>>Michael",
u = ">>>Jackson",
i = 1958,
l = {name: ">>>joyer"};
function c(e) {console.log(">>>" + e)
}
var f = function () {console.log(">>>i am default")
};
(() => {console.log(">>>i am index.js"), console.log(o);
r.log()})()}
]
动静应用导入模块的接口, 将会丢失tree shaking
.
能够看出, 如果模块 (mod.js
) 导出接口 (如year
和log
)相互之间没有依赖, 且没有依赖模块中其余代码时, 会剔除无用代码.
下面的代码中, 呈现大量模块代码之间的合并. 模块
mod1.js
中的代码间接替换到应用语句, 甚至连函数都精简了, 这可能是因为webpack
或代码压缩器的一些精简策略.
模块中跟导出接口无关的代码
一个模块中, 除了导出的各种接口外, 还有一些额定的没有被导出接口所依赖, 这样代码在 tree shaking
中的舍弃策略是如何的呢?
模块代码:
let count = 0;
let deep = '123';
function withSideEffect() {
count ++;
String.prototype.addOneMethod = () => {return deep;};
window.newProp = "new";
console.log('>>>>withSideEffect.js');
}
function withoutSideEffect() {
count ++;
deep = 'new deep';
return count;
}
withoutSideEffect();
withSideEffect();
console.log('>>>>mod.js');
count++;
export default function() {}
该模块中有大量的额定代码, 有一些额定的代码还是有副作用的, 然而默认导出没有依赖它们.
入口代码:
import mod from './mod1.js';
((async () => {console.log('>>>in index.js');
mod();})());
打包后的要害代码:
[function (e, t, n) {
"use strict";
n.r(t);
String.prototype.addOneMethod = () => {}, window.newProp = "new", console.log(">>>>withSideEffect.js"), console.log(">>>>mod.js");
(async() => {console.log(">>>in index.js")
})()}
]
剖析下面的代码, 发现对于 count
相干的语句都被舍弃了, 具备副作用的语句 (console.log
, window.
, String.prototype
) 都被保留了, 这表明 tree shaking
能够剖析依赖到语句级, 对于没有被导出接口依赖的语句, 或者具备副作用语句依赖的无副作用代码 (比方count ++
) 通通都舍弃掉. 能够通过一个副作用中依赖失常语句, 来进一步钻研.
第三方库默认是看作具备副作用的.
如果模块语句是:
let deep = 'old';
let count = 0;
function withSideEffect() {
count ++;
String.prototype.addOneMethod = () => {return deep;};
window.newProp = "new";
console.log('>>>>withSideEffect.js');
}
function withoutSideEffect() {
count ++;
deep = 'new';
return count;
}
withoutSideEffect();
withSideEffect();
console.log('>>>>mod.js');
count++;
export default function() {}
打包后的要害代码:
[function (e, t, n) {
"use strict";
n.r(t);
let o = "old";
o = "new", String.prototype.addOneMethod = () => o, window.newProp = "new", console.log(">>>>withSideEffect.js"), console.log(">>>>mod.js");
(async() => {console.log(">>>in index.js")
})()}
]
你会发现, 变量 deep
都被保留下来了. 这是因为 deep
这个变量会对 String.prototype.addOneMethod
这个副作用语句产生副作用, 故保留下来.
tree shaking
作用域语句级别的依赖剖析, 十分弱小且智能的深刻帮咱们剔除无用代码.
模块导出接口为一个对象
如果模块导出为一个对象, 会怎么解决呢?
模块代码:
const api = {};
api.name = '123';
api.foo = () => {console.log('>>>foo');
}
api.bar = () => {console.log('>>>bar');
}
export default api;
入口文档:
import api from './mod.js';
api.foo();
打包后的要害代码:
[function (e, t, r) {
"use strict";
r.r(t);
const n = {
name: "123",
foo: () => {console.log(">>>foo")
}, bar: () => {console.log(">>>bar")
}
};
n.foo()}
]
发现不会剔除对象中没有应用到的 name
和bar
, 能够揣测 tree shaking
剖析: 因为 api.name = '123';
, api.bar = ...
这两个语句, 对 api
这个变量进行了赋值, 但入口文件有对 api
这个变量进行应用, 依赖了所有对 api
变量进行操作的语句, 因而没有对这些实际上无用的代码进行剔除.
对
Class
也是雷同的解决
模块接口有依赖
如果模块的接口有对其余的接口依赖, tree shaking
将会怎么解决呢?
模块的代码:
export var firstName = '>>>Michael';
export var lastName = '>>>Jackson';
export var name = firstName + lastName;
export var person = {
name,
getName() {
firstName = '>>>new';
return firstName;
}
};
let info = '>>>info';
export function log() {console.log(info);
return firstName;
}
export default function() {
info = '123';
log();}
入口应用:
import {person} from './mod1.js';
console.log('>>>in index.js');
console.log(person);
发现两个接口的源码被并入到入口模块中去了:
[function (e, n, t) {
"use strict";
t.r(n);
var r = ">>>Michael",
o = {
name: r + ">>>Jackson",
getName: () => r = ">>>new"};
console.log(">>>in index.js"), console.log(o)
}
]
如果是应用模块函数接口(log
), 该函数援用了模块外部变量和其余接口变量, 打包代码变为:
[function (e, n, t) {
"use strict";
t.r(n);
var r = ">>>Michael";
let o = ">>>info";
function u() {return console.log(o), r
}
console.log(">>>in index.js"), console.log(u())
}
]
应用模块默认接口函数也是一样, 打包代码未:
[function (e, n, t) {
"use strict";
t.r(n);
let r = ">>>info";
var o = function () {r = "123", console.log(r)
};
console.log(">>>in index.js"), console.log(o())
}
]
在简略的应用模块中, 没有应用的代码将会被舍弃 (语句级).
如果模块中多个接口依赖同一个模块变量, 且有副作用, 将会如何?
模块代码:
let count = 0;
export function addCount() {console.log('>>>addCount');
count ++;
}
export function getCount() {console.log('>>>getCount');
return count;
}
export function setCount(_count) {console.log('>>>setCount');
count = _count;
}
入口代码:
import {addCount, getCount} from './mod1.js';
((async () => {console.log('>>>in index.js');
addCount();
console.log(getCount());
})());
打包后要害代码:
[function (e, t, n) {
"use strict";
n.r(t);
let o = 0;
(async() => {console.log(">>>in index.js"), console.log(">>>addCount"), o++, console.log((console.log(">>>getCount"), o))
})()}
]
能够发现, 依旧会删除无用代码.
下面的探索示例中会把 setCount
这个接口给删除掉, 且只有 addCount
且addCount
中无 console.log
时, 整个模块都会被舍弃掉, 就算有 console.log
语句, 也会舍弃模块中对于 count
相干的代码:
import {addCount} from './mod1.js';
((async () => {console.log('>>>in index.js');
addCount();})());
打包后的要害代码:
[function (e, t, n) {
"use strict";
n.r(t);
(async() => {console.log(">>>in index.js"), console.log(">>>addCount")
})()}
]
这是因为只应用 addCount 时, count++
并不会行影响整个程序 (无副作用), 所以count
相干的代码都被舍弃了, 而应用了 getCount
, 那么count
相干语句就会对程序产生影响, 故而都保留了下来.
能够得出结论: tree shaking
会剖析对模块中接口的语句档次的依赖剖析, 对没有依赖的语句或者无副作用的依赖语句进行删除.
异步导入模块
下面都只剖析了一个入口, 一份打包文件的状况, 但理论场景下, 可能有多份入口, 或者多个异步导入导致打包进去多份文件. 然而这里只探索异步导入的状况, 因为多份入口相似.
模块代码(mod1.js
):
export var firstName = '>>>Michael';
export var lastName = '>>>Jackson';
export var year = 1958;
export var person = {name: '>>>joyer'};
export function log(info) {console.log(`>>>${info}`);
}
export default function() {console.log('>>>i am default');
}
入口代码:
((async () => {const mod = await import('./mod1.js');
console.log(mod.firstName);
})());
打包后的要害代码:
[function (e, t, r) {(async() => {const e = await r.e(1).then(r.bind(null, 1));
console.log(e.firstName)
})()}
]
[,
function (n, o, e) {
"use strict";
e.r(o), e.d(o, "firstName", (function () {return t})), e.d(o, "lastName", (function () {return r})), e.d(o, "year", (function () {return u})), e.d(o, "person", (function () {return c})), e.d(o, "log", (function () {return i}));
var t = ">>>Michael",
r = ">>>Jackson",
u = 1958,
c = {name: ">>>joyer"};
function i(n) {console.log(">>>" + n)
}
o.default = function () {console.log(">>>i am default")
}
}
]
能够发现, tree shaking
曾经生效. 被异步导入的模块不具备剔除代码的成果.
如果被导入的异步模块中在导入一个模块呢? 这是平时开发中, spa
我的项目的规范模块导入构造.
在模块 (mod2.js
) 中导入 (mod1.js
) 代码:
import {log} from './mod1.js';
export function log2() {log(20);
}
export function extra() {}
模块 (mod1.js
) 代码:
export var firstName = '>>>Michael';
export var lastName = '>>>Jackson';
export var year = 1958;
export var person = {name: '>>>joyer'};
export function log(info) {console.log(`>>>${info}`);
}
export default function() {console.log('>>>i am default');
}
入口文件代码:
import {firstName} from './mod1.js';
console.log('>>>index.js');
((async () => {const mod = await import('./mod2.js');
console.log(mod.log2());
console.log(firstName);
})());
打包后的要害代码:
// main.js
[function (e, n, t) {
"use strict";
t.d(n, "a", (function () {return r})), t.d(n, "b", (function () {return o}));
var r = ">>>Michael";
function o(e) {console.log(">>>" + e)
}
},
function (e, n, t) {
"use strict";
t.r(n);
var r = t(0);
console.log(">>>index.js"), (async() => {const e = await t.e(1).then(t.bind(null, 2));
console.log(e.log2()), console.log(r.a)
})()}
]
// 1.js
{2: function (n, t, o) {
"use strict";
o.r(t), o.d(t, "log2", (function () {return u})), o.d(t, "extra", (function () {return i}));
var c = o(0);
function u() {Object(c.b)(20)
}
function i() {}
}
}
能够发现, 对于异步导入的模块 mod2.js
中, 只管 extra
接口没有应用, 也会被导入进来, 也就是说异步导入模块会进行全量导入. 对于模块 mod1.js
来说, 无论是间接在入口文件间接导入应用, 还是在异步模块 mod2.js
导入, 会依据使依赖状况, 进行语句依赖剖析, 剔除无用的语句代码.
能够把异步导入也作为一个入口文件来对待的话.
聚合模块剖析
在一些反对 tree shaking
的第三方库中, 为了反对导入不便, 都有一个模块聚合了其余的所有模块. 如 antd
库, 在 index.js
中, 聚合了其余的所有模块的 default
接口, 相似于
export {default as mod1} from './mod1.js';
export {default as mod2} from './mod2.js';
模块 1 mod1.js
:
export function foo() {console.log('>>>mod1 foo');
}
export default function() {console.log('>>>mod1.js');
}
模块 2 mod2.js
:
export function bar() {console.log('>>>mod2 bar');
}
export default function() {console.log('>>>mod2.js');
}
入口文件:
import {mod1} from './mod.js';
mod1();
打包后的要害代码:
([function (e, t, r) {
"use strict";
r.r(t);
console.log(">>>mod1.js")
}
]
只打包了模块 1 的 default
接口的代码, tree shaking
胜利.
入口文件进行全量导入的场景:
import * as M from './mod.js';
M.mod1();
打包后的要害代码:
[function (e, t, r) {
"use strict";
r.r(t);
console.log(">>>mod1.js")
}
]
一样的成果, 能够揣测, tree shaking
在模块依赖过程中进行语句依赖的传递.
在聚合模块中, 尝试默认导入和具名导入:
export {default as mod1, foo} from './mod1.js';
export {default as mod2, bar} from './mod2.js';
入口文件批改为:
import {foo, bar} from './mod.js';
foo();
bar();
打包后的要害代码:
[function (e, t, r) {
"use strict";
r.r(t);
console.log(">>>mod1 foo"), console.log(">>>mod2 bar")
}
]
具备 tree shaking
个性.
在聚合模块中, 全量导入:
export * from './mod1.js';
export * from './mod2.js';
入口文件批改为:
import {foo} from './mod.js';
foo();
打包后的要害代码:
[function (e, t, r) {
"use strict";
r.r(t);
console.log(">>>mod1 foo"), (void 0)()}
]
具备tree shaking
留神, 全量导入不会导入
default
接口
es6 中还有一种命名空间式的导入导出(聚合模块中代码):
import * as mod1 from './mod1.js';
import * as mod2 from './mod2.js';
export {
mod1,
mod2,
};
尽管
es2020
中有下面导入的简写export * as mod1 from "./mod1.js";
, 但这在最新的 webpack 中还不被反对.
入口文件代码:
import {mod1} from './mod.js';
mod1.foo();
打包后要害代码:
[function (e, t, n) {
"use strict";
n.r(t);
var r = {};
function o() {console.log(">>>mod1 foo")
}
n.r(r), n.d(r, "foo", (function () {return o})), n.d(r, "default", (function () {return u}));
var u = function () {console.log(">>>mod1.js")
};
r.foo()}
]
公布具备 tree shaking
成果, 然而不彻底, 携带了 mod1.js
的defaule
接口, 然而这个接口在整个利用中并没有用到. 风行的 element-ui
就是采纳这种形式聚合所有的组件.
element-ui
组件库并不反对tree shaking
, 一是因为它并没有设置sideEffects
, 二是, 在element-ui
的聚合模块中, 还有一个注册所有组件为全局组件的副作用, 这回导致tree shaking
生效.
写在最初
总结:
- webpack 的
tree shaking
十分弱小. 剔除代码是语句级别, 并能够依据模块依赖进行深层次的依赖剖析. 但这会导致代码侵入性十分高. - 对于类或者对象, 不会做到无用代码剔除.