如何評價 Webpack 2 新引入的 Tree-shaking 代碼優化技術?

介紹:http://www.2ality.com/2015/12/webpack-tree-shaking.html

有沒有人使用過Webpack 2.0,覺得這種技術減小文件體積的效果顯著嗎?使用這項技術有沒有什麼值得注意的?


看了一篇 rollup.js 作者解釋 tree-shaking 的文章:Tree-shaking versus dead code elimination。

在 JS 中的這種 tree-shaking 是他命名的。webpack 中應該是類似的做法,就先說說 rollup.js。

首先,rollup.js 的 tree-shaking 不光是模塊級別的,但是也僅處理了頂層 AST。(比 CommonJS/AMD 的按需打包稍細,但不深入 AST 更深層的部分)。Tree-shaking 是無用代碼移除(DCE, dead code elimination)的一個方法,但和傳統的方法不太一樣。Tree-shaking 找到需要的代碼,灌入最終的結果;傳統 DCE 找到執行不到的代碼,從 AST 里清除。(在我看來傳統的這種方式更應該被稱為 tree-shaking,即搖一下把 AST 中的 dead branch 給抖下來。)作者還用了一個和 DCE 相對應的說法:Live code inclusion。這種方式在目前流行的模塊式開發、最後通過 entry 打包出 bundle 的模式下,才有實際意義。

有人總結了一個定義:DCE 消滅不可能執行的代碼,tree-shaking 消滅沒有用到的代碼。作者表示這個定義可以接受。但他表示,因為 tree-shaking 還不完善,所以建議使用中最好先用 rollup.js 再過 UglifyJS。

webpack 支持 tree-shaking 以後,Axel Rauschmayer 寫了個 webpack + Babel 進行 tree-shaking 的例子:

helpers.js

export function foo() {
return "foo";
}
export function bar() {
return "bar";
}

main.js

import {foo} from "./helpers";

let elem = document.getElementById("output");
elem.innerHTML = `Output: ${foo()}`;

在配合 Webpack + Babel 編譯後,輸出的 bundle 中,helper 模塊代碼如下:

function(module, exports, __webpack_require__) {

/* harmony export */ exports["foo"] = foo;
/* unused harmony export bar */;

function foo() {
return "foo";
}
function bar() {
return "bar";
}
}

可以看出 exports 中已經沒有了 bar 這個方法,於是再配合簡單的代碼壓縮:

function (t, n, r) {
function e() {
return "foo"
}

n.foo = e
}

bar 的代碼就被幹掉了。

而這個例子在 rollup 里,最終以 IIFE 語法輸出的整個 bundle 是這樣的:

(function () {
"use strict";

function foo() {
return "foo";
}

let elem = document.getElementById("output");
elem.innerHTML = `Output: ${foo()}`;

}());

我個人的理解是,在你需要處理的代碼對外不產生副作用時, tree-shaking 效果還不錯,rollup.js 生成的 bundle 會更小一些。看一下它的在線 demo 就知道,模塊合併以後都在同一個作用域下,直接用變數名就可以訪問各個模塊的介面;而不是 webpack 這樣每個模塊外還要包一層函數定義,再通過合併進去的 define/require 相互調用。Tree-shaking 不是代碼壓縮,所以還是得配合壓縮工具來用。


1. 什麼是tree shaking?

就算消除unused code

2. 這個東西的效果怎麼樣?

不怎麼樣。因為js並沒有類型,所以靜態分析做不了。為什麼以前的時候都沒有類似的概念,就是因為沒有類型。 現在加入了module system。這個tree shaking 就是基於模塊的,然而並不能徹底移除,因為模塊本身有side effect,import 的過程中會跑一遍模塊中的代碼,如果這代碼中比如 a 引用了 b, 你最後import 的只有a,然而b也並不會消失,還有一些模塊中本身的side effect 也是類似的。

3. 有哪些坑?

3.1 比起靜態類型語言的deadcode elimination 差很多

3.2 uglifyjs 做過一些dead code elimination了。舉個例子 const a = invokeABC(); 然後a沒有被用過,會簡化為 invokeABC()。但是作為js 也就只能簡化成這樣了,因為你不知道invokeABC()的副作用。

3.3 所以舉例 export a = invokeSomeFunc(); 同樣面臨類似的問題,那就是我就算a沒有被import,但是這個invokeSomeFunc() 同樣要被保留。因為我不知道有什麼side effect。

3.4 比如es6 module中的export default 是存在的,但是commonJS module 中的default和es 6中的是不一樣的。對於這個的tree shaking也是不一樣的。

3.5 其實上面可以看出來 uglifyjs 和 tree shaking 的工作有一部分重複的,執行順序上也有一些問題,導致有些東西其實可以更酷炫,但是現在並不酷炫。

4.其他

那麼這個可以怎麼改進呢?

其實可以給export 標記一個屬性,判斷這個東西是不是lazy的,如果是lazy的就不用計算了。也就不用考慮麻煩的side effect什麼的了。但是這樣模塊複雜性又增加了。

最後。。。

其實沒有用過tree shaking,以上是從webpack issues(Search Results · GitHub)裡面讀到的一些內容。有不對的請大家指教,謝謝大家。


tree-shaking 其實也確實不是什麼特別神的東西,原理而言 @顧軼靈 的回答已經講得比較清楚了,我想指出的一點就是不管是 rollup 還是 webpack 2,tree-shaking 都是因為 ES6 modules 的靜態特性才得以實現的。ES6 modules 的 import 和 export statements 相比完全動態的 CommonJS require,有著本質的區別。舉例來說:

1. 只能作為模塊頂層的語句出現,不能出現在 function 裡面或是 if 裡面。(ECMA-262 15.2)

2. import 的模塊名只能是字元串常量。(ECMA-262 15.2.2)

3. 不管 import 的語句出現的位置在哪裡,在模塊初始化的時候所有的 import 都必須已經導入完成。換句話說,ES6 imports are hoisted。(ECMA-262 15.2.1.16.4 - 8.a)

4. import binding 是 immutable 的,類似 const。比如說你不能 import { a } from "./a" 然後給 a 賦值個其他什麼東西。(ECMA-262 15.2.1.16.4 - 12.c.3)

這些設計雖然使得靈活性不如 CommonJS 的 require,但卻保證了 ES6 modules 的依賴關係是確定 (deterministic) 的,和運行時的狀態無關,從而也就保證了 ES6 modules 是可以進行可靠的靜態分析的。對於主要在服務端運行的 Node 來說,所有的代碼都在本地,按需動態 require 即可,但對於要下發到客戶端的 web 代碼而言,要做到高效的按需使用,不能等到代碼執行了才知道模塊的依賴,必須要從模塊的靜態分析入手。這是 ES6 modules 在設計時的一個重要考量,也是為什麼沒有直接採用 CommonJS。

正是基於這個基礎上,才使得 tree-shaking 成為可能(這也是為什麼 rollup 和 webpack 2 都要用 ES6 module syntax 才能 tree-shaking),所以說與其說 tree-shaking 這個技術怎麼了不起,不如說是 ES6 module 的設計在模塊靜態分析上的種種考量值得讚賞。

---

關於 closure compiler 原來我說的是錯的,最新版本的 closure compiler 支持 ES6 modules 並且也有 tree shaking 的實際效果,但是對規範的支持還不是很完善,比如不支持 wildcard export。而且說真的,advanced mode 用起來限制太多了...


第一次見 tree-shaking 是在 rollup,之後構建工具里使用 rollup 也是覺得還蠻酷的,不過還沒用過 webpack2 =.=

不過真要說有多顯著么,取決於代碼里有多少不會被用到的 export。既然那些 export 沒有被用到過,那麼要麼是忘了刪,要麼是一些的暫時沒有被用到的方法。前者幾率比較低,後者倒是比較常見,特別是對於喜歡把模塊的介面定義的比較完善的同學(當然也有隨便什麼都 export 的同學?)。所以覺得 shake 一下還是有作用的,特別是在一個前端各種模塊和邏輯的相對比較重的應用場景(SPA?)。

tree-shaking 的風險的話倒還好,ES6 模塊的依賴會在編譯的時候靜態解析好,感覺這部分應該問題不大。

手機強答...困了,碎覺 =.=


技術的意義是要放到具體項目中來說的。

對小項目的這種優化,沒什麼意義。非常大的項目,我更期待Closure Compiler能優化TypeScript,或者tsc能直接集成Closure Compiler。用到現在,還是CC的優化能力最強。

javascript - can TypeScript output annotations for Closure Compiler?

https://developers.google.com/v8/experiments#introduction

https://developers.google.com/closure/compiler/#what-is-the-closure-compiler


沒怎麼研究過,不過現在 webpack 4 快要發布了,但看不到社區里說 webpack 的 tree-shaking 是多完美!

理想很豐滿,現實很骨幹。


就是 DCE,由於 ES6 的 export 是個關鍵字,於是給模塊的導入導出表加入了額外的靜態性,更徹底的 DCE 就能實現了。


看了下文章,tree-shaking的原理是基於ES6的export關鍵字來掃描js文件,沒有export的就認為是無用的函數或者屬性,打包的時候剔除掉。

這種有一定的好處,但前提是代碼必須是按照ES6的語法。而現在主流的框架如React都還沒有ES6的shim版本,所以tree-shaking目前沒啥用。

但是既然題主問的是代碼的優化,我想除了打包優化之外還應該包含載入優化,根據React Router的這篇文章(Automatic Code Splitting for React Router w/ ES6 Imports a€」 Modus Create: Front End Development)通過System.import關鍵字Webpack2可以智能的判斷哪些代碼是可以獨立於bundle之外分離成單獨chunk的,相比1.x的require.ensure介面更加智能化。


名字很形象,想像一下秋日裡的樹,搖一搖樹榦,落下一片片枯葉。不過用在 bundler 裡面能搖下多少 dead code?還真不好說。

按照博士文章的介紹,Tree-shaking 似乎就是在 DCE 之前先檢測一下沒有被 import 的 export,然後把這個 export 相關的代碼一併幹掉。

恕在下學識短淺,主流的框架應當都會經過人力優化不會存在無意義的 export,那麼這東西是不是有用全看你自己寫的代碼裡面是不是隨便寫一堆 dead code 並且還 export 出來。

上面有答案說按需載入私以為也是無從談起,需終究是人的需,所謂按需難道是我把 npm 上所有包都搞進來讓 Tree-shaking 給我把不需要的搞掉?

認識淺薄,不到位之處望不吝賜教。


Google的同學估計笑了,炒冷飯,前端對文件大小的關注應該降降溫了


需要明確的一點——tree-shaking之前就可以以插件形式使用了,現在是成為了webpack 2的官方功能。

tree-shaking的出現是非常有意義的!真正做到了前端打包時的按需載入,為前端性能的進一步提升提供了一種新的有效的方法~

(tree-shaking:「做了一點微小的貢獻,謝謝大家( ̄▽ ̄)……」)

ps,同時恭喜webpack成為rollup之後第二個可以(通過非插件形式)實現按需載入的前端工程化工具(●—●)!


其實這個蠻實用的,有其實針對工具庫,框架級別的非常有用


FYI, 一直用jspm做bundler,類似這個主題方面jspm處理的很自然優美,沒有引入多餘機制。


這個只能算很基本的dependency pruning,效果肯定比不上傳統編譯器的dead code elimination。目前最好的JavaScript編譯/優化器還是Closure Compiler。寫好類型注釋+導出需要的部分,然後開啟高級優化(ADVANCED_OPTIMIZATIONS)模式,最後都能完爆其他優化器。


抖個機靈,號稱下一代方案的rollup使用了tree shaking,你認為同一領域目前的王者為了保持地位能不去嘗試?


消滅沒有用的代碼。優秀的程序員應該不會寫出沒用的代碼。


rollup和webpack2都用過 tree shaking是去檢查模塊是否被import過 所以寫的時候要講究這一點 對於node_modules 得看模塊是否支持結構方式import進來 至於體驗上 rollup和webpack2都使用過 沒有感覺出來差異吧 webpack2是在build的時候去做shaking的 dev的時候不做 其實現在用webpack 打包出來的文件都很大很難看懂 也不能說在tree shaking後差別很大吧 倒是rollup在合併代碼的時候 代碼可讀性比webpack高一點

至於使用的時候要注意的就是要改babel-loader的preset-es2015,去掉其中處理import/export的插件("babel-plugin-transform-es2015-modules-commonjs"),由webpack2來接管import/export的編譯 有一個叫做babel-preset-es2015-webpack2專門做這個


推薦閱讀:

TAG:前端開發 | JavaScript | 編譯 | Babel | webpack |