注:原文發表於 2015年 12月20日,彼時一些相關的技術方案還處於公測或者論證階段。javascript
Rich Harris’ module bundler Rollup popularized an important feature in the JavaScript world: tree-shaking, excluding unused exports from bundles. Rollup depends on the static structure of ES6 modules (imports and exports can’t be changed at runtime) to detect which exports are unused.html
Rich Harris 的模塊打包器 Rollup 在 javascript 的圈子裏引爆了一個新的概念 : tree-shaking。 tree shaking 在模塊打包時排除沒有使用到的模塊,從而減少代碼體積,提升加載和執行效率。 Rollup 依賴 ES6 的靜態模塊結構(不像ES6中能夠在runtime中動態的決定導入導出的內容) 分析辨別無用的代碼。java
Tree-shaking for webpack is currently in beta. This blog post explains how it works. The project we are going to examine is on GitHub: tree-shaking-demowebpack
webpack 的 tree-shaking 方案目前處於公測階段。本篇博文解釋它是如何工做的。文中使用到的代碼來自於 tree-shaking-demogit
webpack 2, a new version that is in beta, eliminates unused exports in two steps:es6
webpack2 經過下面的兩個步驟來消除無用的代碼。github
First, all ES6 module files are combined into a single bundle file. In that file, exports that were not imported anywhere are not exported, anymore.web
首先,全部的 ES6 文件整合到一個 bundle 中,在這個文件裏,沒有被導入過的模塊將不會再被導出。json
個人理解是:babel
m1.js
export const foo = () => {} export const bar = () => {}
m2.js
import { foo } from './m1' foo()
整合過程當中,發現 bar 沒有被其餘模塊導入過,因此最終結果是
bundle.js
export const foo = () => {} const bar = () => {} // bar 還在,只是沒有被 export foo()
Second, the bundle is minified, while eliminating dead code. Therefore, entities that are neither exported nor used inside their modules do not appear in the minified bundle. Without the first step, dead code elimination would never remove exports (registering an export keeps it alive).
移除既不導出也不使用的模塊。若是沒有第一步的支持,咱們沒法辨別從未被導入過的模塊。
Unused exports can only be reliably detected at build time if the module system has a static structure. Therefore, webpack 2 can parse and understand all of ES6 and only tree-shakes if it detects an ES6 module. However, only imports and exports are transpiled to ES5. If you want all of the bundle to be in ES5, you need a transpiler for the remaining parts of ES6. In this blog post, we’ll use Babel 6.
只有在靜態的模塊結構下,纔可以準確的移除無用的模塊。 webpack 2 僅僅可以理解和分析 ES6 的模塊而且執行 tree-shaking,並不能幫你將代碼編譯成 ES6,若是你須要那麼作,可使用 Babel 或者其餘的編譯工具。
示例包含兩個 ES6 代碼文件。
helpers.js
// helpers.js export function foo() { return 'foo'; } export function bar() { return 'bar'; }
main.js, 入口文件
// main.js import {foo} from './helpers'; let elem = document.getElementById('output'); elem.innerHTML = `Output: ${foo()}`;
注意 bar 模塊沒有被任何其餘模塊使用。
Babel6 編譯 ES6 代碼最多見辦法的是使用一個預設的 preset babel-preset-es2015 :
{ presets: ['es2015'], }
However, that preset includes the plugin transform-es2015-modules-commonjs, which means that Babel will output CommonJS modules and webpack won’t be able to tree-shake:
然而,這個 preset 包含 transform-es2015-modules-commonjs,它會使得 Babel 輸出 commonjs 風格的模塊,這致使 webpack 沒法執行 tree-shake。
function(module, exports) { 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.foo = foo; exports.bar = bar; function foo() { return 'foo'; } function bar() { return 'bar'; } }
You can see that bar is part of the exports, which prevents it being recognized as dead code by minification.
你能夠看到 bar 如今是 exports 的一部分了, webpack 沒法辨別 bar 是不是 dead code - 歷來沒有被使用的代碼。
What we want is Babel’s es2015, but without the plugin transform-es2015-modules-commonjs. At the moment, the only way to get that is by mentioning all of the preset’s plugins in our configuration data, except for the one we want to exclude. The preset’s source is on GitHub, so it’s basically a case of copying and pasting:
咱們想要不包含 transform-es2015-modules-commonjs 的 babel-preset-es2015 ,目前來講只能顯示的聲明一堆 plugin 來代替 preset。以下。
{ plugins: [ 'transform-es2015-template-literals', 'transform-es2015-literals', 'transform-es2015-function-name', 'transform-es2015-arrow-functions', 'transform-es2015-block-scoped-functions', 'transform-es2015-classes', 'transform-es2015-object-super', 'transform-es2015-shorthand-properties', 'transform-es2015-computed-properties', 'transform-es2015-for-of', 'transform-es2015-sticky-regex', 'transform-es2015-unicode-regex', 'check-es2015-constants', 'transform-es2015-spread', 'transform-es2015-parameters', 'transform-es2015-destructuring', 'transform-es2015-block-scoping', 'transform-es2015-typeof-symbol', ['transform-regenerator', { async: false, asyncGenerators: false }], ], }
If we build the project now, module helpers looks like this inside the bundle:
若是咱們再去編譯這個項目, 會發現一些變化。
function(module, exports, __webpack_require__) { /* harmony export */ exports["foo"] = foo; /* unused harmony export bar */; function foo() { return 'foo'; } function bar() { return 'bar'; } }
Only foo is an export now, but bar is still there. After minification, helpers looks like this (I’ve added line breaks and whitespace to make the code easier to read):
簡單來講, 只有 foo 被導出。接下來作移除的時候,打包工具發現 bar 能夠移除,從而獲得下面的結果。
function (t, n, r) { function e() { return "foo" } n.foo = e }
Et voilà – no more function bar!
再也沒有 bar 了。