體積減小80%!釋放webpack tree-shaking的真正潛力

在上週末廣州舉辦的feday中,webpack的核心開發者Sean在介紹webpack插件系統原理時,隆重介紹了一箇中國學生於Google夏令營,在導師Tobias帶領下寫的一個webpack插件,webpack-deep-scope-analysis-plugin,這個插件可以大大提升webpack tree-shaking的效率。前端

tree-shaking目前的缺陷

tree-shaking 做爲 rollup 的一個殺手級特性,可以利用ES6的靜態引入規範,減小包的體積,避免沒必要要的代碼引入,webpack2也很快引入了這個特性,可是目前,webpack只能作比較簡單的解決方案,好比: webpack

這個例子中,webpack會尋找引入變量的引用,當發現沒有對isNumber的引用時,就會去除isNumber的代碼。這其實不太實用,畢竟在如今的vscode中,沒有引用的變量在ide中都會灰顯提示,通常不會犯這種import某個模塊卻不用的錯誤了。git

若是是接下來這種引入方式呢,我寫了一個demo以下github

這個例子很是簡單,若是用圖來表示是這樣 web

在index.js中引入了func.js中的func2,並無引入func1,可是func1引入了lodash。webpack檢查的時候發現func.js中的確用到了lodash,因此不會把lodash去掉。實際上,咱們根本沒用到它。json

webpack-deep-scope-analysis-plugin就能夠解決這種判斷。babel

插件效果

引入前 app

引入後 ide

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

// Class

class Foo { // <- scope start

} // <- scope end

// If else

if (true) { // <- scope start
 
} /* <- scope end */ else { // <- scope start
 
} // <- scope end

// For

for (;;) { // <- scope start
} // <- scope end

// Catch

try {

} catch (e) { // <- scope start

} // <- scope end

// Function

function() { // <- scope start
} // <- scope end

// Scope

switch() { // <- 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.js
import _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作到和其餘靜態語言同樣好,靠這些工具是遠遠不夠的,模塊自身也必須配合作到符合規範。

文章出處:


《IVWEB 技術週刊》 震撼上線了,關注公衆號:IVWEB社區,每週定時推送優質文章。

相關文章
相關標籤/搜索