在上週末廣州舉辦的feday中,webpack的核心開發者Sean在介紹webpack外掛系統原理時,隆重介紹了一箇中國學生於Google夏令營,在導師Tobias帶領下寫的一個webpack外掛,webpack-deep-scope-analysis-plugin,這個外掛能夠大大提高webpack tree-shaking的效率。
tree-shaking目前的缺陷
tree-shaking 作為 rollup 的一個殺手級特性,能夠利用ES6的靜態引入規範,減少包的體積,避免不必要的程式碼引入,webpack2也很快引入了這個特性,但是目前,webpack只能做比較簡單的解決方案,比如:
這個例子中,webpack會尋找引入變數的引用,當發現沒有對isNumber的引用時,就會去除isNumber的程式碼。這其實不太實用,畢竟在現在的vscode中,沒有引用的變數在ide中都會灰顯提示,一般不會犯這種import某個模組卻不用的錯誤了。
如果是接下來這種引入方式呢,我寫了一個demo如下
這個例子非常簡單,如果用圖來表示是這樣
在index.js中引入了func.js中的func2,並沒有引入func1,但是func1引入了lodash。webpack檢查的時候發現func.js中的確用到了lodash,所以不會把lodash去掉。實際上,我們根本沒用到它。
webpack-deep-scope-analysis-plugin就可以解決這種判斷。
外掛效果
引入前
引入後
85.8kb ->
不到1kb
當然,我這裡是標題黨了,因為這裡直接把一個lodash庫給去掉了,所以變化才這麼驚人。但是即使在實際專案中,我們也能輕易用一個外掛減少大量的不必要的引入。
原理
那麼這個外掛是怎麼去解決這個問題的呢?這裡根據原作者在Medium上寫的文章,簡單介紹一下他的做法。
webpack的原理,其實就是遍歷所有的模組,把它們打包成一個檔案,在這個過程中,它就知道哪些export的模組有被使用到。那我們同樣也可以遍歷所有的scope(作用域),簡化沒有用到的scope,最後只留下我們需要的。
上圖中,func5層層引用fun4 fun3 fun2 fun1,最後解析出來其實只使用了deepEqual模組。
什麼是scope呢,其實scope在各個語言中都有存在,在Wikipedia中是作為計算機術語,有更詳細的解釋,我覺得可以翻譯為作用域或者上下文,在ECMAScript中,有以下明確的定義:
// module scope start// Block{
// <
- scope start
} // <
- scope end// Classclass Foo {
// <
- scope start
} // <
- scope end// If elseif (true) {
// <
- scope start
} /* <
- scope end */ else {
// <
- scope start
} // <
- scope end// Forfor (;
;
) {
// <
- scope start
} // <
- scope end// Catchtry {
} catch (e) {
// <
- scope start
} // <
- scope end// Functionfunction() {
// <
- scope start
} // <
- scope end// Scopeswitch() {
// <
- scope start
} // <
- scope end// module scope end複製程式碼
在ES6中,module是一種根作用域,只有function和class才能作為子作用域被匯出,所以我們解析的時候,不會把所有的scope都作為節點算進去。
我們提到的這個webpack外掛,正是內建了這樣一個scope分析器,它能夠從入口檔案中分析出scope的引用關係,最後排除掉所有沒有用到的模組。
當然,這個外掛也並不是自己做了所有的事情,它也是依賴於了前人的工作。 escope 是一個分析ES中scope的工具,外掛作者將它改成了ts版本整合到了外掛中,並且利用了webpack暴露的介面,可以解析出來的模組的AST樹,基於這個AST就可以交給escope分析出scope的引用關係。
一些邊際用例
凡事不能完美,這個外掛也有一些情況會導致判斷失誤
情況一:重複賦值變數
比較典型的是以下這個例子:
import {
isNull
} from 'lodash-es';
var fun = 1;
fun = function scope(...args) {
return isNull(...args);
}export {
fun
}複製程式碼
這個例子中fun變數一開始被賦值為數字,然後被賦值成一個函式,但是scope分析器會直接跳過這個變數,不把它當作一個單獨的scope。
情況二:純函式
// copy from rambda/es/allPass.jsimport _curry1 from './internal/_curry1';
import curryN from './curryN';
import max from './max';
import pluck from './pluck';
var allPass = /*#__PURE__*/_curry1(function allPass(preds) {
return curryN(reduce(max, 0, pluck('length', preds)), function () {
var idx = 0;
var len = preds.length;
while (idx <
len) {
if (!preds[idx].apply(this, arguments)) {
return false;
} idx += 1;
} return true;
});
});
export default allPass;
複製程式碼
在這個例子中,import allPass
會導致_curry1
的執行,因此它不會被當作一個單獨的scope,因為它可能會有一些“副作用”,比如改變某個全部變數,對全域性造成影響。所以作者給了個方案,可以在這個函式前加/*#__PURE__*/
,這樣就會把這個函式視為無副作用的純函式,如果我們沒有import allPass
,它引用的其他模組都會被去除。
最佳實踐
首先,要用到tree-shaking,必然要保證引用的模組都是ES6規範的。這也是為什麼我在前面的demo中,引入的是lodash-es
而不是lodash
。
在專案中,注意要把babel
設定module: false
,避免babel將模組轉為CommonJS規範。引入的模組包,也必須是符合ES6規範,並且在最新的webpack中加了一條限制,即在package.json
中定義sideEffect: false
,這也是為了避免出現import xxx
導致模組內部的一些函式執行後影響全域性環境,卻被去除掉的情況。
未來
當時跟這位外掛作者溝通,他說將來有可能Tobias會把這個外掛內建到webpack中,這也是符合webpack4零配置的趨勢。但是我們也看得到,要將前端工程的dead code elimination做到和其他靜態語言一樣好,靠這些工具是遠遠不夠的,模組自身也必須配合做到符合規範。
文章出處:
- github專案地址:github.com/vincentdcha…
- 原文出處:vincentdchan.github.io/2018/05/bet…
《IVWEB 技術週刊》 震撼上線了,關注公眾號:IVWEB社群,每週定時推送優質文章。