【譯】如何在 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前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索