下面是一个使用 react 的业务的代码依赖,但是实际上业务代码中并没有对依赖图中标识的模块,也就是说构建工具将不需要的代码打包到了最终的代码当中。显然,这是很不合理的。
- $ webpack --profile --json --config webpack/config.common.js > stats.json
- $ # 将 stats.json 上传到 http://alexkuz.github.io/webpack-chart/ 可视化 entry 的依赖
随着 es6 的普及使用,由于 es6 的 模块是语言层面支持的,方便做静态分析,让进一步的代码优化成为可能,也就是我们今天要讨论的 tree-shaking。
tree-shaking 较早由 Rich_Harris 的 实现,webpack2 也引入了 tree-shaking 的能力。其实在更早,有 google closure compiler 来做类似的事情,不过由于 closure compiler 对代码书写要求比较多,感觉一直没有流行开。
tree-shaking 可以形象的理解为摇树。在 webpack 项目中,有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于树枝。实际情况中,虽然依赖了某个模块,但其实只使用其中的某些功能。通过 tree-shaking,将没有使用的模块摇掉,这样来达到删除无用代码的目的。
所有示例在
main.js
- import {
- A
- }
- from './components/index';
- let a = new A();
- a.render();
index.js
- export A from './A';
- export B from './B';
components/A.js
- function A() {
- this.render = function() {
- return "AAAA";
- }
- }
- export
- default A;
components/B.js
- function B() {
- this.render = function() {
- return "BBBB";
- }
- }
- export
- default B;
- $ npm run 001
结果
查看
- dist/001.min.js
被成功消除了,不能找到
- class B
- BBBB
- !
- function(n) {
- function t(e) {
- if (r[e]) return r[e].exports;
- var u = r[e] = {
- i: e,
- l: !1,
- exports: {}
- };
- return n[e].call(u.exports, u, u.exports, t),
- u.l = !0,
- u.exports
- }
- var r = {};
- return t.m = n,
- t.c = r,
- t.i = function(n) {
- return n
- },
- t.d = function(n, r, e) {
- t.o(n, r) || Object.defineProperty(n, r, {
- configurable: !1,
- enumerable: !0,
- get: e
- })
- },
- t.n = function(n) {
- var r = n && n.__esModule ?
- function() {
- return n.
- default
- }:
- function() {
- return n
- };
- return t.d(r, "a", r),
- r
- },
- t.o = function(n, t) {
- return Object.prototype.hasOwnProperty.call(n, t)
- },
- t.p = "",
- t(t.s = 3)
- } ([function(n, t, r) {
- "use strict";
- var e = r(1);
- r(2);
- r.d(t, "a",
- function() {
- return e.a
- })
- },
- function(n, t, r) {
- "use strict";
- function e() {
- this.render = function() {
- return "AAAA"
- }
- }
- t.a = e
- },
- function(n, t, r) {
- "use strict"
- },
- function(n, t, r) {
- "use strict";
- Object.defineProperty(t, "__esModule", {
- value: !0
- });
- var e = r(0),
- u = new e.a;
- u.render()
- }]);
稍微修改下
的定义方式:
- A、B
- function A() {
- }
- A.prototype.render = function() {
- return "AAAA";
- }
- export default A;
- function B() {
- }
- B.prototype.render = function() {
- return "BBBB";
- }
- export default B;
跟上面的区别在于采用原型链的方式添加了一个
方法
- render
- $ npm run 002
结果
查看
发现
- dist/002.min.js
并没有被成功消除
- class B
- !
- function(n) {
- function t(e) {
- if (r[e]) return r[e].exports;
- var u = r[e] = {
- i: e,
- l: !1,
- exports: {}
- };
- return n[e].call(u.exports, u, u.exports, t),
- u.l = !0,
- u.exports
- }
- var r = {};
- return t.m = n,
- t.c = r,
- t.i = function(n) {
- return n
- },
- t.d = function(n, r, e) {
- t.o(n, r) || Object.defineProperty(n, r, {
- configurable: !1,
- enumerable: !0,
- get: e
- })
- },
- t.n = function(n) {
- var r = n && n.__esModule ?
- function() {
- return n.
- default
- }:
- function() {
- return n
- };
- return t.d(r, "a", r),
- r
- },
- t.o = function(n, t) {
- return Object.prototype.hasOwnProperty.call(n, t)
- },
- t.p = "",
- t(t.s = 3)
- } ([function(n, t, r) {
- "use strict";
- var e = r(1);
- r(2);
- r.d(t, "a",
- function() {
- return e.a
- })
- },
- function(n, t, r) {
- "use strict";
- function e() {}
- e.prototype.render = function() {
- return "AAAA"
- },
- t.a = e
- },
- function(n, t, r) {
- "use strict";
- function e() {}
- e.prototype.render = function() {
- return "BBBB"
- }
- },
- function(n, t, r) {
- "use strict";
- Object.defineProperty(t, "__esModule", {
- value: !0
- });
- var e = r(0),
- u = new e.a;
- u.render()
- }]);
再修改下
的定义方式,改为 es6 的
- A、B
语法,看起来更加简洁了:
- class
- class A {
- render() {
- return "AAAA";
- }
- }
- export
- default A;
- class B {
- render() {
- return "BBBB";
- }
- }
- export
- default B;
- $ npm run 003
结果
查看
发现
- dist/003.min.js
并没有被成功消除,并且文件还变大了
- class B
- !
- function(n) {
- function t(e) {
- if (r[e]) return r[e].exports;
- var o = r[e] = {
- i: e,
- l: !1,
- exports: {}
- };
- return n[e].call(o.exports, o, o.exports, t),
- o.l = !0,
- o.exports
- }
- var r = {};
- return t.m = n,
- t.c = r,
- t.i = function(n) {
- return n
- },
- t.d = function(n, r, e) {
- t.o(n, r) || Object.defineProperty(n, r, {
- configurable: !1,
- enumerable: !0,
- get: e
- })
- },
- t.n = function(n) {
- var r = n && n.__esModule ?
- function() {
- return n.
- default
- }:
- function() {
- return n
- };
- return t.d(r, "a", r),
- r
- },
- t.o = function(n, t) {
- return Object.prototype.hasOwnProperty.call(n, t)
- },
- t.p = "",
- t(t.s = 3)
- } ([function(n, t, r) {
- "use strict";
- var e = r(1);
- r(2);
- r.d(t, "a",
- function() {
- return e.a
- })
- },
- function(n, t, r) {
- "use strict";
- function e(n, t) {
- if (! (n instanceof t)) throw new TypeError("Cannot call a class as a function")
- }
- var o = function() {
- function n() {
- e(this, n)
- }
- return n.prototype.render = function() {
- return "AAAA"
- },
- n
- } ();
- t.a = o
- },
- function(n, t, r) {
- "use strict";
- function e(n, t) {
- if (! (n instanceof t)) throw new TypeError("Cannot call a class as a function")
- } (function() {
- function n() {
- e(this, n)
- }
- return n.prototype.render = function() {
- return "BBBB"
- },
- n
- })()
- },
- function(n, t, r) {
- "use strict";
- Object.defineProperty(t, "__esModule", {
- value: !0
- });
- var e = r(0),
- o = new e.a;
- o.render()
- }]);
简单的变量场景
- const A = "AAAA";
- export
- default A;
- const B = "BBBB";
- export
- default B;
- import {
- A,
- B
- }
- from './components/index';
- let a = new A();
- a.render();
- $npm run 004
结果
查看
,
- dist/004.min.js
被成功消除
- B
- !
- function(t) {
- function n(e) {
- if (r[e]) return r[e].exports;
- var u = r[e] = {
- i: e,
- l: !1,
- exports: {}
- };
- return t[e].call(u.exports, u, u.exports, n),
- u.l = !0,
- u.exports
- }
- var r = {};
- return n.m = t,
- n.c = r,
- n.i = function(t) {
- return t
- },
- n.d = function(t, r, e) {
- n.o(t, r) || Object.defineProperty(t, r, {
- configurable: !1,
- enumerable: !0,
- get: e
- })
- },
- n.n = function(t) {
- var r = t && t.__esModule ?
- function() {
- return t.
- default
- }:
- function() {
- return t
- };
- return n.d(r, "a", r),
- r
- },
- n.o = function(t, n) {
- return Object.prototype.hasOwnProperty.call(t, n)
- },
- n.p = "",
- n(n.s = 3)
- } ([function(t, n, r) {
- "use strict";
- var e = r(1);
- r(2);
- r.d(n, "a",
- function() {
- return e.a
- })
- },
- function(t, n, r) {
- "use strict";
- var e = "AAAA";
- n.a = e
- },
- function(t, n, r) {
- "use strict"
- },
- function(t, n, r) {
- "use strict";
- Object.defineProperty(n, "__esModule", {
- value: !0
- });
- var e = r(0);
- console.log(e.a)
- }]);
tree-shaking 不能消除带有副作用的代码。
比如示例 2,在函数的原型链上添加了方法,在这个场景下,
- B
其实应该被删除掉,但是换一个场景,比如王
的原型链上加一个
- Array
方法:
- unique
- function B() {
- }
- B.prototype.render = function() {
- return "BBBB";
- }
- Array.prototype.unique = function() {
- // 将 array 中的重复元素去除
- }
- export default B;
如果移除
并且移除其原型成员
- function B
,是否应该移除
- B.prototype.render
呢?在其它代码里,可能使用
- Array.prototype.unique
,并且调用
- arr = new Array()
,所以移除
- arr.unique()
是不安全的。而现在实现的 tree-shaking 并不能区分
- Array.prototype.unique
和
- B.prototype.render
,既然后者不能移除,那么前者也不能移除。并且
- Array.prototype.unique
也不能被移除。
- function B
示例 3 使用 es6 的 class 语法定义,按理说,应该没有副作用了吧,可是查看
,
- dist/003.min.js
还是没有被消除。为什么呢?因为 babel 将
- B
定义转变成了
- class
定义,而这个定义是有副作用的。
- function
- $ npm run 005 # 即执行下面的命令
- $ # ./node_modules/webpack/bin/webpack.js --config webpack/005.js
- $ # 跟 npm run 004 的命令的区别在于缺少 -p 压缩参数
- $ # ./node_modules/webpack/bin/webpack.js -p --config webpack/004.js
查看生成的代码
,
- dist/005.min.js
被转换成了如下的,跟示例 2 类似的代码了,
- class B
是一个自执行的函数,带有副作用,所以并不能被安全的移除。代码片段:
- B
- /* 2 */
- /***/
- function(module, exports, __webpack_require__) {
- "use strict";
- function _classCallCheck(instance, Constructor) {
- if (! (instance instanceof Constructor)) {
- throw new TypeError("Cannot call a class as a function");
- }
- }
- var B = function() {
- function B() {
- _classCallCheck(this, B);
- }
- B.prototype.render = function render() {
- return "BBBB";
- };
- return B;
- } ();
- /* unused harmony default export */
- var _unused_webpack_default_export = B;
既然 babel 会转换代码,那么能不能不使用 babel 呢?
修改 webpack config
,禁用 babel loader
- webpack/006.js
- module: {
- // rules: [
- // {
- // test: /\.js$/,
- // loader: 'babel-loader',
- // query: {
- // babelrc: false,
- // presets: [["es2015", { "modules": false, "loose": true }]]
- // }
- // }
- // ]
- },
- $ npm run 006
结果
- $ npm run 006
- > @ 006 E:\work\05_code\webpack-demo-project
- > node ./node_modules/webpack/bin/webpack.js -p --config webpack/006.js
- Hash: e46574279f3737838494
- Version: webpack 2.2.0-rc.3
- Time: 76ms
- Asset Size Chunks Chunk Names
- 006.min.js 3.65 kB 0 [emitted] 006
- [3] ./src/006/main.js 72 bytes {0} [built]
- + 3 hidden modules
- ERROR in 006.min.js from UglifyJs
- SyntaxError: Unexpected token: name (A) [006.min.js:89,6]
- npm ERR! Windows_NT 6.1.7601
- npm ERR! argv "D:\\Program Files\\nodejs\\node.exe" "D:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js" "run" "006"
- npm ERR! node v6.9.2
- npm ERR! npm v3.10.9
- npm ERR! code ELIFECYCLE
- npm ERR! @ 006: `node ./node_modules/webpack/bin/webpack.js -p --config webpack/006.js`
- npm ERR! Exit status 2
- npm ERR!
- npm ERR! Failed at the @ 006 script'node ./node_modules/webpack/bin/webpack.js -p --config webpack/006.js'.
- npm ERR! Make sure you have the latest version of node.js and npm installed.
- npm ERR! If you do, this is most likely a problem with the package,
- npm ERR! not with npm itself.
- npm ERR! Tell the author that this fails on your system:
- npm ERR! node ./node_modules/webpack/bin/webpack.js -p --config webpack/006.js
- npm ERR! You can get information on how to open an issue for this project with:
- npm ERR! npm bugs
- npm ERR! Or if that isn't available, you can get their info via:
- npm ERR! npm owner ls
- npm ERR! There is likely additional logging output above.
- npm ERR! Please include the following file with any support request:
- npm ERR! E:\work\05_code\webpack-demo-project\npm-debug.log
因为带了
参数进行压缩,内部使用的是 uglifyjs ,并不识别 es6 语法。
- -p
- /* 2 */
- /***/
- function(module, exports, __webpack_require__) {
- "use strict";
- class B {
- render() {
- return "BBBB";
- }
- }
- /* unused harmony default export */
- var _unused_webpack_default_export = B;
既然 babel 转换的方案不行,存不存在将
定义转换后无副作用的的方案呢?
- class
答案是,有的。 是一个完全基于 es6 的删减方案。
上面说到,
定义被转换成了带副作用的函数,而 babili 不一样的地方在于,不对 es6 的代码做 babel 转换,而是输入、输出都是 es6。
- class
- // Example ES2015 Code
- class Mangler {
- constructor(program) {
- this.program = program;
- }
- }
- new Mangler(); // without this it would just output nothing since Mangler isn't used
Before
- // ES2015+ code -> Babel -> Babili/Uglify -> Minified ES5 Code
- var a = function a(b) {
- _classCallCheck(this, a),
- this.program = b
- };
- new a;
After
- // ES2015+ code -> Babili -> Minified ES2015+ Code
- class a {
- constructor(b) {
- this.program = b
- }
- }
- new a;
最后生成的代码如果要运行在 es5 环境中,需要再用 babel 转换成 es5。
webpack/007.js
- let BabiliPlugin = require("babili-webpack-plugin");
- ...
- plugins: [
- new BabiliPlugin()
- ],
- $ npm run 007
结果
查看
可以看到生成的代码,不包含
- dist/007.js
的定义了:
- class B
- (function(b) {
- function c(e) {
- if (d[e]) return d[e].exports;
- var f = d[e] = {
- i: e,
- l: !1,
- exports: {}
- };
- return b[e].call(f.exports, f, f.exports, c),
- f.l = !0,
- f.exports
- }
- var d = {};
- return c.m = b,
- c.c = d,
- c.i = function(e) {
- return e
- },
- c.d = function(e, f, g) {
- c.o(e, f) || Object.defineProperty(e, f, {
- configurable: !1,
- enumerable: !0,
- get: g
- })
- },
- c.n = function(e) {
- var f = e && e.__esModule ?
- function() {
- return e['default']
- }: function() {
- return e
- };
- return c.d(f, 'a', f),
- f
- },
- c.o = function(e, f) {
- return Object.prototype.hasOwnProperty.call(e, f)
- },
- c.p = '',
- c(c.s = 3)
- })([function(b, c, d) {
- 'use strict';
- var e = d(1);
- d(2),
- d.d(c, 'a',
- function() {
- return e.a
- })
- },
- function(b, c) {
- 'use strict';
- c.a = class {
- render() {
- return 'AAAA'
- }
- }
- },
- function() {
- 'use strict'
- },
- function(b, c, d) {
- 'use strict';
- Object.defineProperty(c, '__esModule', {
- value: !0
- });
- var e = d(0);
- let f = new e.a;
- f.render()
- }]);
将上面的代码缩进后查看,
成功被删除掉了,
- class B
的定义经过压缩处理,变为:
- class A
- function(b, c) {
- 'use strict';
- c.a = class {
- render() {
- return 'AAAA'
- }
- }
- }
的语法还是 es6
- A
语法,如果要在浏览器中运行,还需要进一步处理。
- class
看似很完美,但是 babili 的缺点在于,不能使用 babel 的很多 plugin ,而社区中很多方案都是基于 babel ,比如 babel-preset-react ,所以此方案还是有实用性上的问题。
不过,这也可能是一个潜在的方案。将来有机会再研究下去。
查看其它使用 tree-shaking 的例子,能达到效果的都是使用函数来组织模块的,比如 ,比如 。所以,其实使用 tree-shaking 的局限性还是比较大。社区里也在反馈, 。
目前看到使用 tree-shaking 比较成功的例子是 ,不过使用的是 rollupjs 的方案。浏览 d3-jsnext 的代码,其代码基本都是用
而不是
- function
来组织的。
- class
来源: