【译】如何在 Webpack 2 中使用 tree-shaking

如何在 Webpack 2 中使用 tree-shaking

tree-shaking 这个术语首先源自 Rollup -- Rich Harris 写的模块打包工具。它是指在打包时只包含用到的 Javascript 代码。它依赖于 ES6 静态模块(exports 和 imports 不能在运行时修改),这使咱们在打包时能够检测到未使用的代码。Webpack 2 也引入了这一特性,Webpack 2 已经内置支持 ES6 模块和 tree-shaking。本文将会介绍如何在 webpack 中使用这一特性,如何克服使用中的难点。前端

若是想跳过,直接看例子请访问 BabelTypescriptreact

应用举例

理解在 Webpack 中使用 tree-shaking 的最佳的方式是经过一个微型应用例子。我将它比做一个汽车有特定的引擎,该应用由 2 个文件组成。第 1 个文件有:一些 class,表明不一样种类的引擎;一个函数返回其版本号 -- 都经过 export 关键字导出。android

export class V6Engine {
  toString() {
    return 'V6';
  }
}

export class V8Engine {
  toString() {
    return 'V8';
  }
}

export function getVersion() {
  return '1.0';
}复制代码

第 2 个文件表示一个汽车拥有它本身的引擎,将这个文件做为应用打包的入口(entry)。webpack

import { V8Engine } from './engine';

class SportsCar {
  constructor(engine) {
    this.engine = engine;
  }

  toString() {
    return this.engine.toString() + ' Sports Car';
  }
}

console.log(new SportsCar(new V8Engine()).toString());复制代码

经过定义类 SportsCar,咱们只使用了 V8Engine,而没有用到 V6Engine。运行这个应用会输出:‘V8 Sports Car’ios

应用了 tree-shaking 后,咱们指望打包结果只包含用到的类和函数。在这个例子中,意味着它只有 V8EngineSportsCar 类。让咱们来看看它是如何工做的。git

打包

咱们打包时不使用编译器(Babel 等)和压缩工具(UglifyJS 等),能够获得以下输出:github

(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* unused harmony export getVersion */
class V6Engine {
  toString() {
    return 'V6';
  }
}
/* unused harmony export V6Engine */

class V8Engine {
  toString() {
    return 'V8';
  }
}
/* harmony export (immutable) */ __webpack_exports__["a"] = V8Engine;

function getVersion() {
  return '1.0';
}

/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__engine__ = __webpack_require__(0);

class SportsCar {
  constructor(engine) {
    this.engine = engine;
  }

  toString() {
    return this.engine.toString() + ' Sports Car';
  }
}

console.log(new SportsCar(new __WEBPACK_IMPORTED_MODULE_0__engine__["a" /* V8Engine */]()).toString());

/***/ })复制代码

Webpack 用注释 /\unused harmony export V6Engine*/ 将未使用的类和函数标记下来,用 /*harmony export (immutable)*/ webpack_exports[“a”] = V8Engine;* 来标记用到的。你应该会问未使用的代码怎么还在?tree-shaking 没有生效吗?web

移除未使用代码(Dead code elimination)vs 包含已使用代码(live code inclusion)

背后的缘由是:Webpack 仅仅标记未使用的代码(而不移除),而且不将其导出到模块外。它拉取全部用到的代码,将剩余的(未使用的)代码留给像 UglifyJS 这类压缩代码的工具来移除。UglifyJS 读取打包结果,在压缩以前移除未使用的代码。经过这一机制,就能够移除未使用的函数 getVersion 和类 V6Enginetypescript

而 Rollup 不一样,它(的打包结果)只包含运行应用程序所必需的代码。打包完成后的输出并无未使用的类和函数,压缩仅涉及实际使用的代码。json

设置

UglifyJS 不支持 ES6(又名 ES2015)及以上。咱们须要用 Babel 将代码编译为 ES5,而后再用 UglifyJS 来清除无用代码。

最重要的是让 ES6 模块不受 Babel 预设(preset)的影响。Webpack 认识 ES6 模块,只有当保留 ES6 模块语法时才可以应用 tree-shaking。若是将其转换为 CommonJS 语法,Webpack 不知道哪些代码是使用过的,哪些不是(就不能应用 tree-shaking了)。最后,Webpack将把它们转换为 CommonJS 语法。

咱们须要告诉 Babel 预设(在这个例子中是babel-preset-env)不要转换 module。

{
  "presets": [
    ["env", {
      "loose": true,
      "modules": false
    }]
  ]
}复制代码

对应 Webpack 配置:

module: {
  rules: [
    { test: /\.js$/, loader: 'babel-loader' }
  ]
},

plugins: [
  new webpack.LoaderOptionsPlugin({
    minimize: true,
    debug: false
  }),
  new webpack.optimize.UglifyJsPlugin({
    compress: {
      warnings: true
    },
    output: {
      comments: false
    },
    sourceMap: false
  })
]复制代码

来看一下 tree-shaking 以后的输出: link to minified code.

能够看到函数 getVersion 被移除了,这是咱们所预期的,然而类 V6Engine 并无被移除。这是什么缘由呢?

问题

首先 Babel 检测到 ES6 模块将其转换为 ES5,而后 Webpack 将全部的模块汇集起来,最后 UglifyJS 会移除未使用的代码。咱们来看一下 UglifyJS 的输出,就能够找到问题出在哪里。

WARNING in car.prod.bundle.js from UglifyJs
Dropping unused function getVersion [car.prod.bundle.js:103,9]
Side effects in initialization of unused variable V6Engine [car.prod.bundle.js:79,4]

它告诉咱们类 V6Engine 转换为 ES5 的代码在初始化时有反作用。

var V6Engine = function () {
  function V6Engine() {
    _classCallCheck(this, V6Engine);
  }

  V6Engine.prototype.toString = function toString() {
    return 'V6';
  };

  return V6Engine;
}();复制代码

在使用 ES5 语法定义类时,类的成员函数会被添加到属性 prototype,没有什么方法能彻底避免此次赋值。UglifyJS 不可以分辨它仅仅是类声明,仍是其它有反作用的操做 -- UglifyJS 不能作控制流分析。

编译过程阻止了对类进行 tree-shaking。它仅对函数起做用。

在 Github 上,有一些相关的 bug report:Webpack repositoryUglifyJS repository。一个解决方案是 UglifyJS 彻底支持 ES6,但愿下个主版本可以支持。另外一个解决方案是将其标记为 pure(无反作用),以便 UglifyJS 可以处理。这种方法已经实现,但要想生效,还需编译器支持将类编译后的赋值标记为 @__PURE__。实现进度:BabelTypescript

使用 Babili

Babel 的开发者们认为:为何不开发一个基于 Babel 的代码压缩工具,这样就可以识别 ES6+ 的语法了。因此他们开发了Babili,全部 Babel 能够解析的语言特性它都支持。Babili 能将 ES6 代码编译为 ES5,移除未使用的类和函数,这就像 UglifyJS 已经支持 ES6 同样。

Babili 会在编译前删除未使用的代码。在编译为 ES5 以前,很容易找到未使用的类,所以 tree-shaking 也能够用于类声明,而再也不仅仅是函数。

咱们只需用 Babili 替换 UglifyJS,而后删除 babel-loader 便可。另外一种方式是将 Babili 做为 Babel 的预设,仅使用 babel-loader(移除 UglifyJS 插件)。推荐使用第一种(插件的方式),由于当编译器不是 Babel(好比 Typescript)时,它也能生效。

module: {
  rules: []
},

plugins: [
  new BabiliPlugin()
]复制代码

咱们须要将 ES6+ 代码传给 BabiliPlugin,不然它不用移除(未使用的)类。

使用 Typescript 等编译器时,也应当使用 ES6+。Typescript 应当输出 ES6+ 代码,以便 tree-shaking 可以生效。

如今的输出再也不包含类 V6Engine压缩后代码

第三方包

对第三方包来讲也是,应当使用 ES6 模块。幸运的是,愈来愈多的包做者同时发布 CommonJS 格式 和 ES6 格式的模块。ES6 模块的入口由 package.json 的字段 module 指定。

对 ES6 模块,未使用的函数会被移除,但 class 并不必定会。只有当包内的 class 定义也为 ES6 格式时,Babili 才能将其移除。不多有包可以以这种格式发布,但有的作到了(好比说 lodash 的 lodash-es)。

罪魁祸首是当包的单独文件经过扩展它们来修改其余模块时,导入文件有反作用。RxJs就是一个例子。经过导入一个运算符来修改其中一个类,这些被认为是反作用,它们阻止代码进行 tree-shaking。

总结

经过 tree-shaking 你能够至关程度上减小应用的体积。Webpack 2 内置支持它,但其机制并不一样于 Rollup。它会包含全部的代码,标记未使用的函数和函数,以便压缩工具可以移除。这就是对全部代码都进行 tree-shake 的困难之处。使用默认的压缩工具 UglifyJS,它仅移除未使用的函数和变量;Babili 支持 ES6,能够用它来移除(未使用的)类。咱们还必须特别注意第三方模块发布的方式是否支持 tree-shaking。

但愿这篇文章为您清楚阐述了 Webpack tree-shaking 背后的原理,并为您提供了克服困难的思路。

实际例子请访问 BabelTypescript


感谢阅读!喜欢本文请点击原文中的 ❤,而后分享到社交媒体上。欢迎关注 MediumTwitter 阅读更多有关 Javascript 的内容!


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索