配置Tree Shaking來減少JavaScript的打包體積

Fundebug發表於2018-08-15

譯者按: 用Tree Shaking技術來減少JavaScript的Payload大小

為了保證可讀性,本文采用意譯而非直譯。另外,本文版權歸原作者所有,翻譯僅用於學習。

小編推薦:Fundebug專注於JavaScript、微信小程式、微信小遊戲,Node.js和Java線上bug實時監控。真的是一個很好用的bug監控服務,眾多大佬公司都在使用。

如今一個網頁應用可以體積很大,特別是JavaScript程式碼。2018年年中,HTTP Archive統計在移動端JavaScript檔案的平均傳輸大小將近350KB。你要知道,這僅僅是傳輸的大小。在網路傳輸的時候,JavaScript往往是經過壓縮的。也就是說,在瀏覽器解壓縮之後,實際的大小會遠遠大於這個值。而這一點相當重要。如果考慮到瀏覽器處理資料的資源消耗,其中壓縮是不得不考慮的。一個300KB的檔案解壓縮會達到900KB,並且在分析和編譯的時候,體積依然是900KB。

配置Tree Shaking來減少JavaScript的打包體積

其實,處理JavaScript是很耗資源的。不像圖片只會在下載的時候有一點簡單的解碼處理,JavaScript需要分析,編譯,然後再被執行。一個位元組一個位元組地處理,所以JavaScript的處理很貴。

配置Tree Shaking來減少JavaScript的打包體積

為了優化JavaScript引擎,各種改進方法被提出來。提升JavaScript程式碼的效能,是開發者最擅長的事情。畢竟,有誰比架構師更擅長優化架構的效能呢?

Code splitting是其中一個用來提升效能的方法,通過將JavaScript應用拆分成一個個塊,然後在需要的時候才下載。這個方法很好,但是有一個很常見的問題沒有處理,那就是有很多打包的程式碼我們壓根沒有用到。為了解決這個問題,我們用tree shaking。

什麼叫tree shaking ?

Tree shaking是一種消除無用程式碼(dead code)的方式。這個詞是由最先從Rollup社群開始流行的,不過本身的理念很早就有了。在webpack中也有相同的理念,在本文我們會用一個例子來描述。

"tree shaking"這個詞來自於應用的架構以及本身的依賴關係就像一個樹形結構。樹的每一個節點表示應用中一個唯一的功能。在現代網頁應用中,依賴關係通常使用static import statement,如下所示:

// Import all the array utilities!
import arrayUtils from "array-utils";
複製程式碼

注意:如果你不瞭解ES6,我強烈推薦你閱讀Pony Foo上面的這篇文章。我們這篇文章假定你對ES6有一定的瞭解。如果沒有,趕緊學學去吧。

當你的app還很小的時候,也許只有很少的依賴檔案。而且應該幾乎使用了所有你自己新增的依賴。但是,當你的app開發了一段時間,越來越多的依賴新增進去。由於各種原因,舊的依賴可能根本沒有使用了,但是呢依然在你的程式碼庫裡面,沒有被刪除。最終導致你的app夾帶了很多並沒有使用的JavaScript。通過分析我們如何使用import語句,tree shaking會移除無用程式碼。

// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";
複製程式碼

這個import語句和之前的區別在於,與其引入整個array-utils,而整個array-utils可能有非常多的函式,不如只引入我們需要的部分。在開發構建的時候,這兩種使用方法並沒有區別。但是在生產打包的時候,我們可以配置webpack來剔除不需要的函式,使得整個程式碼檔案變小。在這篇文章中,我們會指導你如何做。

案例

為了演示起見,我寫了一個簡單的單頁應用。你可以克隆程式碼並跟著操作。我會詳細描述每一步,所以克隆不是必備步驟。

示例是一個可以搜尋吉他效果器的資料庫。

配置Tree Shaking來減少JavaScript的打包體積

應用在構建的時候,所有的JavaScript檔案打包成了一個vendor和一個app檔案。

配置Tree Shaking來減少JavaScript的打包體積

上圖中的檔案是打包後的結果,已經經過uglification。21.1KB的大小完全可以接受。不過,當前是沒有使用tree shaking來優化的結果。我們來看看如何進一步優化。

在任何應用中,尋找使用tree shaking優化的機會首先要尋找import語句。一般都在component檔案的頂部,像這樣:

import * as utils from "../../utils/utils";
複製程式碼

也許你已經看過這樣的語句。其實ES6中有多種匯入模組的方法,不過這樣的匯入語句最值得注意。因為它意味著匯入utils模組中的所有函式,並放到utils的名稱空間下面。所有,一個最大的疑問是:在模組中到底有多少函式?

如果你檢視utils模組的原始碼,你會發現真的很多。大概有1300行的程式碼量。

不過,別擔心。也許所有的函式都在當前檔案中使用了,對吧?我們真的需要所有的函式嗎?我們來檢查一下,通過查詢utils.,看看有幾處使用。結果呢:

配置Tree Shaking來減少JavaScript的打包體積

好吧,總共只找到了3處。 我們再看看具體是哪個函式?如果我們一個一個地檢視,會發現其實只用了一個函式,就是utils.simpleSort

if (this.state.sortBy === "model") {
  // Simple sort gets used here...
  json = utils.simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
  // ..and here...
  json = utils.simpleSort(json, "type", this.state.sortOrder);
} else {
  // ..and here.
  json = utils.simpleSort(json, "manufacturer", this.state.sortOrder);
}
複製程式碼

也就是說,我們引入了一個1300行的檔案,結果只使用了其中一個函式。

當然,我們要承認這個例子為了演示目的,可能有故意之嫌。不過,它表述了一個事實,那就是在很多真實的應用中,存在著像這樣需要優化的地方。那麼如何做呢?

禁止Babel將ES6編譯到CommonJS

Babel在很多應用中已經必不可少。不幸的是,它會讓tree shaking變得困難。如果你使用babel-preset-env,它會將你的ES6編譯到可相容性更好的CommonJS。

問題在於對於CommonJS,tree shaking非常困難,而且webpack不知道哪些需要消除掉。不過呢,好在有一個很簡單的解法:配置babel-preset-env,讓其保持ES6不動,不要翻譯。具體的配置放在你配置Babel的地方(.babelrc或則package.json):

{
  "presets": [
    ["env", {
      "modules": false
    }]
  ]
}
複製程式碼

簡單地配置"modules":false即可,webpack會分析所有檔案中模組的依賴關係,然後剔除那些沒有使用的程式碼。並且,這個處理不會有相容問題,因為webpack最終會將程式碼轉換到相容的版本。

謹記副作用(Side Effect)

另一個需要考慮的是:應用中使用模組是否有副作用。我舉一個例子來說什麼叫副作用(這個例子表述了在一個函式中去修改函式外部的變數):

let fruits = ["apple", "orange", "pear"];

console.log(fruits); // (3) ["apple", "orange", "pear"]

const addFruit = function(fruit) {
  fruits.push(fruit);
};

addFruit("kiwi");

console.log(fruits); // (4) ["apple", "orange", "pear", "kiwi"]
複製程式碼

在這個例子中,addFruit修改了fruit陣列,而fruit陣列是全域性的。

只有當函式給定輸入後,產生相應的輸出,而不修改任何外部的東西,我們才可以安全的做shaking操作。

所以,在webpack中,我們可以通過配置"sideEffects":false表示模組是安全的,沒有副作用的。

{
  "name": "webpack-tree-shaking-example",
  "version": "1.0.0",
  "sideEffects": false
}
複製程式碼

或則,你可以告訴webpack哪些檔案有副作用:

{
  "name": "webpack-tree-shaking-example",
  "version": "1.0.0",
  "sideEffects": [
    "./src/utils/utils.js"
  ]
}
複製程式碼

在上面的配置中,webpack會假定其它檔案都是無副作用的。如果你不想新增到package.json檔案中,你可以配置module.rules

按需匯入

我們可以只匯入我們需要使用的函式,在示例中,我麼只需要simpleSort

import { simpleSort } from "../../utils/utils";
複製程式碼

使用上面的語法,我們就只會將simpleSort函式匯出,我們只需要將utils.simpleSort改為simpleSort

if (this.state.sortBy === "model") {
  json = simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
  json = simpleSort(json, "type", this.state.sortOrder);
} else {
  json = simpleSort(json, "manufacturer", this.state.sortOrder);
}
複製程式碼

接下來我們看看執行效果,首先回顧之前的打包效果:

配置Tree Shaking來減少JavaScript的打包體積
接下來看使用了tree shaking後的效果:

配置Tree Shaking來減少JavaScript的打包體積
兩個模組都變小了,特別是main檔案。通過將utils中無用程式碼刪掉,整個體積削減了60%。這不僅節省了下載時間,而且節省了處理時間。

其他情況

在大多數情況下,上面的方法就足夠了。但是,總有例外的情況會讓你抓耳撓腮。比如,Lodash就不行。因為Lodash當時的架構就不支援,所以需要一些額外的工作:a) 安裝lodash-es來替代lodash;b) 使用稍微不同的語法(叫做cherry-picking):

// This still pulls in all of lodash even if everything is configured right.
import { sortBy } from "lodash";

// This will only pull in the sortBy routine.
import sortBy from "lodash-es/sortBy";
複製程式碼

如果你傾向於使用一致的import語法,你可以使用標準的lodash包,然後安裝babel-plugin-lodash

如果有些模組使用CommonJS格式(module.exports),那麼webpack無法使用tree shaking。一些外掛(webpack-common-shake)為CommonJS提供tree shaking。但是,因為有些CommonJS的模式是無法做tree shaking的。如果你想很保險地剔除掉沒有使用的依賴,ES6才是你最佳的選擇。

關於Fundebug

Fundebug專注於JavaScript、微信小程式、微信小遊戲、支付寶小程式、React Native、Node.js和Java實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了6億+錯誤事件,得到了Google、360、金山軟體等眾多知名使用者的認可。歡迎免費試用!

相關文章