譯者按: 用Tree Shaking技術來減少JavaScript的Payload大小
為了保證可讀性,本文采用意譯而非直譯。另外,本文版權歸原作者所有,翻譯僅用於學習。
小編推薦:Fundebug專注於JavaScript、微信小程序、微信小游戲,Node.js和Java線上bug實時監控。真的是一個很好用的bug監控服務,眾多大佬公司都在使用。
如今一個網頁應用可以體積很大,特別是JavaScript代碼。2018年年中,HTTP Archive統計在移動端JavaScript文件的平均傳輸大小將近350KB。你要知道,這僅僅是傳輸的大小。在網絡傳輸的時候,JavaScript往往是經過壓縮的。也就是說,在瀏覽器解壓縮之后,實際的大小會遠遠大於這個值。而這一點相當重要。如果考慮到瀏覽器處理數據的資源消耗,其中壓縮是不得不考慮的。一個300KB的文件解壓縮會達到900KB,並且在分析和編譯的時候,體積依然是900KB。
其實,處理JavaScript是很耗資源的。不像圖片只會在下載的時候有一點簡單的解碼處理,JavaScript需要分析,編譯,然后再被執行。一個字節一個字節地處理,所以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";
當你的app還很小的時候,也許只有很少的依賴文件。而且應該幾乎使用了所有你自己添加的依賴。但是,當你的app開發了一段時間,越來越多的依賴添加進去。由於各種原因,舊的依賴可能根本沒有使用了,但是呢依然在你的代碼庫里面,沒有被刪除。最終導致你的app夾帶了很多並沒有使用的JavaScript。通過分析我們如何使用import語句,tree shaking會移除無用代碼。注意:如果你不了解ES6,我強烈推薦你閱讀Pony Foo上面的這篇文章。我們這篇文章假定你對ES6有一定的了解。如果沒有,趕緊學學去吧。
// 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";
如果你查看utils模塊的源代碼,你會發現真的很多。大概有1300行的代碼量。也許你已經看過這樣的語句。其實ES6中有多種導入模塊的方法,不過這樣的導入語句最值得注意。因為它意味着導入utils模塊中的所有函數,並放到utils的命名空間下面。所有,一個最大的疑問是:在模塊中到底有多少函數?
不過,別擔心。也許所有的函數都在當前文件中使用了,對吧?我們真的需要所有的函數嗎?我們來檢查一下,通過查找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將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最終會將代碼轉換到兼容的版本。
另一個需要考慮的是:應用中使用模塊是否有副作用。我舉一個例子來說什么叫副作用(這個例子表述了在一個函數中去修改函數外部的變量):
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"]
只有當函數給定輸入后,產生相應的輸出,而不修改任何外部的東西,我們才可以安全的做shaking操作。在這個例子中,addFruit
修改了fruit
數組,而fruit
數組是全局的。
所以,在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";
如果有些模塊使用CommonJS格式(module.exports),那么webpack無法使用tree shaking。一些插件(webpack-common-shake)為CommonJS提供tree shaking。但是,因為有些CommonJS的模式是無法做tree shaking的。如果你想很保險地剔除掉沒有使用的依賴,ES6才是你最佳的選擇。如果你傾向於使用一致的import語法,你可以使用標准的lodash包,然后安裝babel-plugin-lodash
。
關於Fundebug
Fundebug專注於JavaScript、微信小程序、微信小游戲、支付寶小程序、React Native、Node.js和Java實時BUG監控。
自從2016年雙十一正式上線,Fundebug累計處理了6億+錯誤事件,得到了Google、360、金山軟件等眾多知名用戶的認可。歡迎免費試用!
版權聲明: 轉載時請注明作者Fundebug以及本文地址: https://blog.fundebug.com/2018/08/15/reduce-js-payload-with-tree-shaking