关于webpack:treeshaking效果探讨

43次阅读

共计 11174 个字符,预计需要花费 28 分钟才能阅读完成。

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';

模块 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.jsdefaule接口, 然而这个接口在整个利用中并没有用到. 风行的 element-ui 就是采纳这种形式聚合所有的组件.

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

写在最初

总结:

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

正文完
 0