注:原文发表于 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 了。