专栏首页IMWeb前端团队webpack2 的 tree-shaking 好用吗?

webpack2 的 tree-shaking 好用吗?

代码压缩的现状

下面是一个使用 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 的 rollupjs 实现,webpack2 也引入了tree-shaking 的能力。其实在更早,有 google closure compiler 来做类似的事情,不过由于 closure compiler 对代码书写要求比较多,感觉一直没有流行开。

什么是 tree-shaking ?

tree-shaking 可以形象的理解为摇树。在 webpack 项目中,有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于树枝。实际情况中,虽然依赖了某个模块,但其实只使用其中的某些功能。通过 tree-shaking,将没有使用的模块摇掉,这样来达到删除无用代码的目的。

实际效果如何

所有示例在 tree-shaking-demo

示例 1

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()}]);

示例 2

稍微修改下 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()}]);

示例 3

再修改下 A、B 的定义方式,改为 es6 的 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()}]);

示例 4

简单的变量场景

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.jsB 被成功消除

!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() ,所以移除 Array.prototype.unique 是不安全的。而现在实现的 tree-shaking 并不能区分 B.prototype.renderArray.prototype.unique ,既然后者不能移除,那么前者也不能移除。并且 function B 也不能被移除。

示例 3 使用 es6 的 class 语法定义,按理说,应该没有副作用了吧,可是查看 dist/003.min.jsB 还是没有被消除。为什么呢?因为babel 将 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.jsclass B 被转换成了如下的,跟示例 2 类似的代码了,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 呢?

示例 6

修改 webpack config webpack/006.js ,禁用 babel loader

    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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 使用 JavaScript 自动化你的 Mac

    系统自带的编写自动化脚本的工具是ScriptEditor。打开编辑器,把语言从 AppleScript切换到JavaScritp。接下来我们就来实战一下,写一个...

    IMWeb前端团队
  • 使用 JavaScript 自动化你的 Mac

    在Apple发布的Yosemite系统(OSX10.10+)中有一个被大家忽略的特性:使用 JavaScript编写自动化脚本。在这之前只能通过AppleSc...

    IMWeb前端团队
  • webpack2 的 tree-shaking 好用吗?

    下面是一个使用 react 的业务的代码依赖,但是实际上业务代码中并没有对依赖图中标识的模块,也就是说构建工具将不需要的代码打包到了最终的代码当中。显然,这是很...

    IMWeb前端团队
  • phpcms v9 常用函数

    常用函数 , 打开include/global.func.php,下面存放一些公共函数 view plaincopy to clipboardprint? fu...

    joshua317
  • js中(function(){})()的写法用处

    后来查了下资料,js中(function(){…})()立即执行函数写法理解,终于了解了。

    帅的一麻皮
  • JavaScript立即调用的函数表达式

    主要参考知乎上这个问题:javascript 匿名函数有哪几种执行方式 长天之云的回答。

    meteoric
  • 一篇文章带你了解JavaScript中的函数表达式,递归,闭包,变量,this对象,模块作用域

    它的一个重要特点就是:函数声明提升,就是在执行代码前先读取函数声明,可以把函数声明放在调用它的语句后。

    达达前端
  • 命名函数表达式

    好文章,可惜中文译文已经无法访问了。不过在cssrain上找到一篇:www.cssrain.cn/demo/named%20function%20express...

    meteoric
  • lodash源码解读之模块化的基础——IIFE

    IIFE包含两部分。 第一部分是一个匿名函数,它包裹在分组操作符()中,拥有独立的词法作用域。 第二部分是再一次使用分组操作符(),创建一个立即执行函数表达式。...

    我是leon
  • webpack2 的 tree-shaking 好用吗?

    下面是一个使用 react 的业务的代码依赖,但是实际上业务代码中并没有对依赖图中标识的模块,也就是说构建工具将不需要的代码打包到了最终的代码当中。显然,这是很...

    IMWeb前端团队

扫码关注云+社区

领取腾讯云代金券