webpack等构建工具提供tree-shaking机制, 利用es6Module的语法的exportimport语法进行动态剖析,对无用代码进行剔除,缩小打包后的代码量.
启动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)导出接口(如yearlog)相互之间没有依赖, 且没有依赖模块中其余代码时, 会剔除无用代码.

下面的代码中, 呈现大量模块代码之间的合并.模块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()    }]

发现不会剔除对象中没有应用到的namebar, 能够揣测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这个接口给删除掉, 且只有addCountaddCount中无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.jsdefaule接口, 然而这个接口在整个利用中并没有用到. 风行的element-ui就是采纳这种形式聚合所有的组件.

element-ui组件库并不反对tree shaking, 一是因为它并没有设置sideEffects, 二是, 在element-ui的聚合模块中, 还有一个注册所有组件为全局组件的副作用, 这回导致tree shaking生效.

写在最初

总结:

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