譯者按: 用Tree Shaking技術來減小JavaScript的Payload大小javascript
爲了保證可讀性,本文采用意譯而非直譯。另外,本文版權歸原做者全部,翻譯僅用於學習。html
小編推薦:Fundebug專一於JavaScript、微信小程序、微信小遊戲,Node.js和Java線上bug實時監控。真的是一個很好用的bug監控服務,衆多大佬公司都在使用。java
現在一個網頁應用能夠體積很大,特別是JavaScript代碼。2018年年中,HTTP Archive統計在移動端JavaScript文件的平均傳輸大小將近350KB。你要知道,這僅僅是傳輸的大小。在網絡傳輸的時候,JavaScript每每是通過壓縮的。也就是說,在瀏覽器解壓縮以後,實際的大小會遠遠大於這個值。而這一點至關重要。若是考慮到瀏覽器處理數據的資源消耗,其中壓縮是不得不考慮的。一個300KB的文件解壓縮會達到900KB,而且在分析和編譯的時候,體積依然是900KB。webpack
其實,處理JavaScript是很耗資源的。不像圖片只會在下載的時候有一點簡單的解碼處理,JavaScript須要分析,編譯,而後再被執行。一個字節一個字節地處理,因此JavaScript的處理很貴。git
爲了優化JavaScript引擎,各類改進方法被提出來。提高JavaScript代碼的性能,是開發者最擅長的事情。畢竟,有誰比架構師更擅長優化架構的性能呢?es6
Code splitting是其中一個用來提高性能的方法,經過將JavaScript應用拆分紅一個個塊,而後在須要的時候才下載。這個方法很好,可是有一個很常見的問題沒有處理,那就是有不少打包的代碼咱們壓根沒有用到。爲了解決這個問題,咱們用tree shaking。github
Tree shaking是一種消除無用代碼(dead code)的方式。這個詞是由最早從Rollup社區開始流行的,不過自己的理念很早就有了。在webpack中也有相同的理念,在本文咱們會用一個例子來描述。web
"tree shaking"這個詞來自於應用的架構以及自己的依賴關係就像一個樹形結構。樹的每個節點表示應用中一個惟一的功能。在現代網頁應用中,依賴關係一般使用static import statement,以下所示:數據庫
// Import all the array utilities!
import arrayUtils from "array-utils";
複製代碼
注意:若是你不瞭解ES6,我強烈推薦你閱讀Pony Foo上面的這篇文章。咱們這篇文章假定你對ES6有必定的瞭解。若是沒有,趕忙學學去吧。npm
當你的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來剔除不須要的函數,使得整個代碼文件變小。在這篇文章中,咱們會指導你如何作。
爲了演示起見,我寫了一個簡單的單頁應用。你能夠克隆代碼並跟着操做。我會詳細描述每一步,因此克隆不是必備步驟。
示例是一個能夠搜索吉他效果器的數據庫。
應用在構建的時候,全部的JavaScript文件打包成了一個vendor和一個app文件。
上圖中的文件是打包後的結果,已經通過uglification。21.1KB的大小徹底能夠接受。不過,當前是沒有使用tree shaking來優化的結果。咱們來看看如何進一步優化。
在任何應用中,尋找使用tree shaking優化的機會首先要尋找import語句。通常都在component文件的頂部,像這樣:
import * as utils from "../../utils/utils";
複製代碼
也許你已經看過這樣的語句。其實ES6中有多種導入模塊的方法,不過這樣的導入語句最值得注意。由於它意味着導入utils模塊中的全部函數,並放到utils的命名空間下面。全部,一個最大的疑問是:在模塊中到底有多少函數?
若是你查看utils模塊的源代碼,你會發現真的不少。大概有1300行的代碼量。
不過,別擔憂。也許全部的函數都在當前文件中使用了,對吧?咱們真的須要全部的函數嗎?咱們來檢查一下,經過查找utils.
,看看有幾處使用。結果呢:
好吧,總共只找到了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在不少應用中已經必不可少。不幸的是,它會讓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最終會將代碼轉換到兼容的版本。
另外一個須要考慮的是:應用中使用模塊是否有反作用。我舉一個例子來講什麼叫反作用(這個例子表述了在一個函數中去修改函數外部的變量):
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後的效果: 兩個模塊都變小了,特別是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專一於JavaScript、微信小程序、微信小遊戲、支付寶小程序、React Native、Node.js和Java實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了6億+錯誤事件,獲得了Google、360、金山軟件等衆多知名用戶的承認。歡迎免費試用!