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';
模块1mod1.js
:
export function foo() {
console.log('>>>mod1 foo');
}
export default function() {
console.log('>>>mod1.js');
}
模块2mod2.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
非常强大. 剔除代码是语句级别,并可以根据模块依赖进行深层次的依赖分析.但这会导致代码侵入性非常高. - 对于类或者对象, 不会做到无用代码剔除.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。