webpack2 的 tree-shaking 好用吗?

tiyu 8年前
   <h2>代码压缩的现状</h2>    <p>下面是一个使用 react 的业务的代码依赖,但是实际上业务代码中并没有对依赖图中标识的模块,也就是说构建工具将不需要的代码打包到了最终的代码当中。显然,这是很不合理的。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/4028e83f92e6991ba73b79e2ef120705.png"></p>    <pre>  <code class="language-javascript">$ webpack --profile --json --config webpack/config.common.js > stats.json  $ # 将 stats.json 上传到 http://alexkuz.github.io/webpack-chart/ 可视化 entry 的依赖</code></pre>    <p>随着 es6 的普及使用,由于 es6 的 模块是语言层面支持的,方便做静态分析,让进一步的代码优化成为可能,也就是我们今天要讨论的 tree-shaking。</p>    <p>tree-shaking 较早由 Rich_Harris 的 <a href="/misc/goto?guid=4958975739586912540" rel="nofollow,noindex">rollupjs</a> 实现,webpack2 也引入了tree-shaking 的能力。其实在更早,有 google closure compiler 来做类似的事情,不过由于 closure compiler 对代码书写要求比较多,感觉一直没有流行开。</p>    <h2>什么是 tree-shaking ?</h2>    <p>tree-shaking 可以形象的理解为摇树。在 webpack 项目中,有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于树枝。实际情况中,虽然依赖了某个模块,但其实只使用其中的某些功能。通过 tree-shaking,将没有使用的模块摇掉,这样来达到删除无用代码的目的。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/397a461849570cdca3ec7cf75e39ccd1.gif"></p>    <p><img src="https://simg.open-open.com/show/486ef2367943a60c8a586d8e222ab7db.png"></p>    <p><img src="https://simg.open-open.com/show/e7ab8ed2f770e6bde979abd67472dd53.png"></p>    <h2>实际效果如何</h2>    <p>所有示例在 <a href="/misc/goto?guid=4959732647089160413" rel="nofollow,noindex">tree-shaking-demo</a></p>    <h2>示例 1</h2>    <p>main.js</p>    <pre>  <code class="language-javascript">import { A } from './components/index';  let a = new A();  a.render();</code></pre>    <p>index.js</p>    <pre>  <code class="language-javascript">export A from './A';  export B from './B';</code></pre>    <p>components/A.js</p>    <pre>  <code class="language-javascript">function A () {      this.render = function() {          return "AAAA";      }  }  export default A;</code></pre>    <p>components/B.js</p>    <pre>  <code class="language-javascript">function B () {      this.render = function() {          return "BBBB";      }  }  export default B;</code></pre>    <pre>  <code class="language-javascript">$ npm run 001</code></pre>    <p>结果</p>    <p>查看 dist/001.min.js class B 被成功消除了,不能找到 BBBB</p>    <pre>  <code class="language-javascript">!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()}]);</code></pre>    <h2>示例 2</h2>    <p>稍微修改下 A、B 的定义方式:</p>    <pre>  <code class="language-javascript">function A() {  }  A.prototype.render = function() {      return "AAAA";  }  export default A;</code></pre>    <pre>  <code class="language-javascript">function B() {  }    B.prototype.render = function() {      return "BBBB";  }  export default B;</code></pre>    <p>跟上面的区别在于采用原型链的方式添加了一个 render 方法</p>    <pre>  <code class="language-javascript">$ npm run 002</code></pre>    <p>结果</p>    <p>查看 dist/002.min.js 发现 class B 并没有被成功消除</p>    <pre>  <code class="language-javascript">!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()}]);</code></pre>    <h2>示例 3</h2>    <p>再修改下 A、B 的定义方式,改为 es6 的 class 语法,看起来更加简洁了:</p>    <pre>  <code class="language-javascript">class A {      render() {          return "AAAA";      }  }  export default A;</code></pre>    <pre>  <code class="language-javascript">class B {      render() {          return "BBBB";      }  }  export default B;</code></pre>    <pre>  <code class="language-javascript">$ npm run 003</code></pre>    <p>结果</p>    <p>查看 dist/003.min.js 发现 class B 并没有被成功消除,并且文件还变大了</p>    <pre>  <code class="language-javascript">!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()}]);</code></pre>    <h2>示例 4</h2>    <p>简单的变量场景</p>    <pre>  <code class="language-javascript">const A = "AAAA";  export default A;</code></pre>    <pre>  <code class="language-javascript">const B = "BBBB";  export default B;</code></pre>    <pre>  <code class="language-javascript">import { A, B } from './components/index';  let a = new A();  a.render();</code></pre>    <pre>  <code class="language-javascript">$npm run 004</code></pre>    <p>结果</p>    <p>查看 dist/004.min.js , B 被成功消除</p>    <pre>  <code class="language-javascript">!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)}]);</code></pre>    <h2>为什么</h2>    <p>tree-shaking 不能消除带有副作用的代码。</p>    <p>比如示例2,在函数的原型链上添加了方法,在这个场景下, B</p>    <p>其实应该被删除掉,但是换一个场景,比如王 Array 的原型链上加一个 unique 方法:</p>    <pre>  <code class="language-javascript">function B() {  }    B.prototype.render = function() {      return "BBBB";  }    Array.prototype.unique = function() {      // 将 array 中的重复元素去除  }    export default B;</code></pre>    <p>如果移除 function B 并且移除其原型成员 B.prototype.render ,是否应该移除 Array.prototype.unique 呢?在其它代码里,可能使用 arr = new Array() ,并且调用 arr.unique() ,所以移除 Array.prototype.unique 是不安全的。而现在实现的 tree-shaking 并不能区分 B.prototype.render 和 Array.prototype.unique ,既然后者不能移除,那么前者也不能移除。并且 function B 也不能被移除。</p>    <p>示例 3 使用 es6 的 class 语法定义,按理说,应该没有副作用了吧,可是查看 dist/003.min.js , B 还是没有被消除。为什么呢?因为babel 将 class 定义转变成了 function 定义,而这个定义是有副作用的。</p>    <pre>  <code class="language-javascript">$ 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</code></pre>    <p>查看生成的代码 dist/005.min.js , class B 被转换成了如下的,跟示例 2 类似的代码了, B 是一个自执行的函数,带有副作用,所以并不能被安全的移除。代码片段:</p>    <pre>  <code class="language-javascript">/* 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;</code></pre>    <p>既然babel会转换代码,那么能不能不使用 babel 呢?</p>    <h2>示例 6</h2>    <p>修改 webpack config webpack/006.js ,禁用 babel loader</p>    <pre>  <code class="language-javascript">module: {          // rules: [          //     {          //         test: /\.js$/,          //         loader: 'babel-loader',          //         query: {          //             babelrc: false,          //             presets: [["es2015", { "modules": false, "loose": true }]]          //         }          //     }          // ]      },</code></pre>    <pre>  <code class="language-javascript">$ npm run 006</code></pre>    <p>结果</p>    <pre>  <code class="language-javascript">$ 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</code></pre>    <p>因为带了 -p 参数进行压缩,内部使用的是 uglifyjs ,并不识别 es6 语法。</p>    <pre>  <code class="language-javascript">/* 2 */  /***/ function(module, exports, __webpack_require__) {    "use strict";  class B {      render() {          return "BBBB";      }  }    /* unused harmony default export */ var _unused_webpack_default_export = B;</code></pre>    <h2>示例 7</h2>    <p>既然 babel 转换的方案不行,存不存在将 class 定义转换后无副作用的的方案呢?</p>    <p>答案是,有的。 <a href="/misc/goto?guid=4959732647186057903" rel="nofollow,noindex">babili</a> 是一个完全基于 es6 的删减方案。</p>    <p>上面说到, class 定义被转换成了带副作用的函数,而 babili 不一样的地方在于,不对 es6 的代码做 babel 转换,而是输入、输出都是 es6。</p>    <pre>  <code class="language-javascript">// Example ES2015 Code  class Mangler {    constructor(program) {      this.program = program;    }  }  new Mangler(); // without this it would just output nothing since Mangler isn't used</code></pre>    <p>Before</p>    <pre>  <code class="language-javascript">// ES2015+ code -> Babel -> Babili/Uglify -> Minified ES5 Code  var a=function a(b){_classCallCheck(this,a),this.program=b};new a;</code></pre>    <p>After</p>    <pre>  <code class="language-javascript">// ES2015+ code -> Babili -> Minified ES2015+ Code  class a{constructor(b){this.program=b}}new a;</code></pre>    <p>最后生成的代码如果要运行在 es5 环境中,需要再用 babel 转换成 es5。</p>    <p>webpack/007.js</p>    <pre>  <code class="language-javascript">let BabiliPlugin = require("babili-webpack-plugin");  ...      plugins: [          new BabiliPlugin()      ],</code></pre>    <pre>  <code class="language-javascript">$ npm run 007</code></pre>    <p>结果</p>    <p>查看 dist/007.js 可以看到生成的代码,不包含 class B 的定义了:</p>    <pre>  <code class="language-javascript">(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()}]);</code></pre>    <p>将上面的代码缩进后查看, class B 成功被删除掉了, class A 的定义经过压缩处理,变为:</p>    <pre>  <code class="language-javascript">function(b, c) {      'use strict';      c.a = class {          render() {              return 'AAAA'          }      }  }</code></pre>    <p>A 的语法还是 es6 class 语法,如果要在浏览器中运行,还需要进一步处理。</p>    <p>看似很完美,但是 babili 的缺点在于,不能使用 babel 的很多 plugin ,而社区中很多方案都是基于 babel ,比如 babel-preset-react ,所以此方案还是有实用性上的问题。</p>    <p>不过,这也可能是一个潜在的方案。将来有机会再研究下去。</p>    <h2>总结</h2>    <p>查看其它使用 tree-shaking 的例子,能达到效果的都是使用函数来组织模块的,比如 <a href="/misc/goto?guid=4959657911972707763" rel="nofollow,noindex">Tree-shaking with webpack 2 and Babel 6</a> ,比如 <a href="/misc/goto?guid=4959732647300574857" rel="nofollow,noindex">webpack example: harmony-unused</a> 。所以,其实使用 tree-shaking 的局限性还是比较大。社区里也在反馈, <a href="/misc/goto?guid=4959732647385171433" rel="nofollow,noindex">webpack2 没有比 webpack 的代码更小多少</a> 。</p>    <h2>适用场景</h2>    <p>目前看到使用 tree-shaking 比较成功的例子是 <a href="/misc/goto?guid=4959732647466258104" rel="nofollow,noindex">d3-jsnext</a> ,不过使用的是 rollupjs 的方案。浏览 d3-jsnext 的代码,其代码基本都是用 function 而不是 class 来组织的。</p>    <h2>参考</h2>    <ol>     <li><a href="/misc/goto?guid=4959732647555411836" rel="nofollow,noindex">Tree shaking completely broken?</a></li>     <li><a href="/misc/goto?guid=4959732647631370241" rel="nofollow,noindex">uglifyjs 配合webpack 压缩代码的一个思路</a></li>     <li><a href="/misc/goto?guid=4959732647721131252" rel="nofollow,noindex">Tree shaking and "unused harmony default export</a></li>     <li><a href="/misc/goto?guid=4959732647300574857" rel="nofollow,noindex">webpack2 官方 tree-shaking 示例</a></li>    </ol>    <p> </p>    <p>来自:http://imweb.io/topic/58666d57b3ce6d8e3f9f99b0</p>    <p> </p>