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
十分弱小. 剔除代码是语句级别,并能够依据模块依赖进行深层次的依赖剖析.但这会导致代码侵入性十分高. - 对于类或者对象, 不会做到无用代码剔除.