- 原文地址:How to do proper tree-shaking in Webpack 2
- 原文做者:Gábor Soós
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:薛定諤的貓
- 校對者:lsvih、lampui
tree-shaking 這個術語首先源自 Rollup -- Rich Harris 寫的模塊打包工具。它是指在打包時只包含用到的 Javascript 代碼。它依賴於 ES6 靜態模塊(exports 和 imports 不能在運行時修改),這使咱們在打包時能夠檢測到未使用的代碼。Webpack 2 也引入了這一特性,Webpack 2 已經內置支持 ES6 模塊和 tree-shaking。本文將會介紹如何在 webpack 中使用這一特性,如何克服使用中的難點。前端
若是想跳過,直接看例子請訪問 Babel、Typescript。react
理解在 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 後,咱們指望打包結果只包含用到的類和函數。在這個例子中,意味着它只有 V8Engine 和 SportsCar 類。讓咱們來看看它是如何工做的。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
背後的緣由是:Webpack 僅僅標記未使用的代碼(而不移除),而且不將其導出到模塊外。它拉取全部用到的代碼,將剩餘的(未使用的)代碼留給像 UglifyJS 這類壓縮代碼的工具來移除。UglifyJS 讀取打包結果,在壓縮以前移除未使用的代碼。經過這一機制,就能夠移除未使用的函數 getVersion 和類 V6Engine。typescript
而 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 repository、UglifyJS repository。一個解決方案是 UglifyJS 徹底支持 ES6,但願下個主版本可以支持。另外一個解決方案是將其標記爲 pure(無反作用),以便 UglifyJS 可以處理。這種方法已經實現,但要想生效,還需編譯器支持將類編譯後的賦值標記爲 @__PURE__。實現進度:Babel、Typescript。
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 背後的原理,併爲您提供了克服困難的思路。
實際例子請訪問 Babel、Typescript。
感謝閱讀!喜歡本文請點擊原文中的 ❤,而後分享到社交媒體上。歡迎關注 Medium,Twitter 閱讀更多有關 Javascript 的內容!
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。