現今的網路應用程式可能會非常龐大,特別是其中的 JavaScript 部分。自 2018 年中起,HTTP 封存而成的行動裝置傳輸 JavaScript 大小中位數約為 350 KB。這只是傳輸大小!JavaScript 透過網路傳送時通常會經過壓縮,這表示瀏覽器解壓縮後,JavaScript 的「實際」數量會高出許多。請務必留意,由於只要涉及資源「處理」,壓縮就不相關。即使解壓縮後的 900 KB 大小,已解壓縮的 JavaScript 仍為剖析器和編譯器 900 KB。
JavaScript 是處理大量資源的資源。圖片下載後只會產生相對繁重的解碼時間,但 JavaScript 必須經過剖析、編譯,最後才執行。對位元組而言,這使得 JavaScript 比其他類型的資源還要昂貴。
![這張圖表比較 JavaScript 170 KB 與同等大小 JPEG 圖片的處理時間。相較於 JPEG,JavaScript 資源的位元組需要耗用大量資源。](https://web.developers.google.cn/static/articles/reduce-javascript-payloads-with-tree-shaking/image/a-diagram-comparing-proc-5b49bd91e7285.png?authuser=19&hl=zh-tw)
我們不斷改善提高 JavaScript 引擎的效率,但改善 JavaScript 效能和開發人員一樣,一直都是一項任務。
因此,有一些技術來提高 JavaScript 效能。「程式碼分割」是這種技術之一,可將應用程式 JavaScript 分為多個區塊,並只將這些區塊提供給需要這些區塊的應用程式路徑,進而提升效能。
雖然這項技術有效,卻無法解決導致大量 JavaScript 應用程式的常見問題,也就是加入從未使用過的程式碼。請搖動樹就能解決這個問題。
什麼是搖樹?
樹木晃動是一種刪除惡意程式碼的形式。「Rollup」一詞很受歡迎,但無效程式碼刪除概念已有一段時間。這個概念也在「webpack」中找到了購買服務,具體會以範例應用程式的方式進行。
「樹動搖擺」一詞來自應用程式的心理模型及其依附元件,類似樹狀結構。樹狀結構中的每個節點都代表可為應用程式提供獨特功能的依附元件。在新型應用程式中,這些依附元件是透過靜態 import
陳述式導入,例如:
// Import all the array utilities!
import arrayUtils from "array-utils";
如果應用程式還很年輕,可能不太需要依附,可能就沒那麼重要了。這個資料庫也會使用您新增的依附元件,而非全部的依附元件。但隨著應用程式日漸成熟,可能會有更多依附元件。複合的項目會使較舊的依附元件不會使用,但可能不會從程式碼集內修剪。最終,應用程式最終會發布許多未使用的 JavaScript。透過利用靜態 import
陳述式提取 ES6 模組特定部分的方式,樹狀架構就能解決這個問題:
// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";
這個 import
範例和上一個範例的差異在於,而不是從 "array-utils"
模組匯入「所有內容」(可能會包含大量程式碼),這個範例只會匯入其中的特定部分。在開發人員版本中,這不會有任何改變,因為系統會無論如何匯入整個模組。在正式版本中,Webpack 可設為「搖動」移除未明確匯入的 ES6 模組匯出,使正式版建構更小。在本指南中,您將瞭解如何達成這個目標!
尋找搖搖樹的機會
如需說明,我們提供了單頁應用程式範例,向您說明樹動功能的運作方式。您可以視需要複製簡報,並按照指示操作,但我們會在本指南中涵蓋所有步驟,因此您不必自行複製 (除非需要動手學習)。
範例應用程式是吉他效果踏板的可供搜尋資料庫,只要輸入查詢,系統就會顯示效果踏板清單。
![螢幕截圖:用於搜尋吉他特效腳本資料庫的範例單頁應用程式。](https://web.developers.google.cn/static/articles/reduce-javascript-payloads-with-tree-shaking/image/a-screenshot-a-sample-p-c7096b568d49b.png?authuser=19&hl=zh-tw)
推動這個應用程式的行為分為多個供應商 (即Preact 和 Emotion) 以及應用程式專屬的程式碼套件 (又稱為「區塊」,Webpack 將其稱為「區塊」):
![Chrome 開發人員工具網路面板中顯示兩個應用程式碼組合 (或區塊) 的螢幕截圖。](https://web.developers.google.cn/static/articles/reduce-javascript-payloads-with-tree-shaking/image/a-screenshot-two-applica-13160017c55f2.png?authuser=19&hl=zh-tw)
上圖所示的 JavaScript 套件是生產版本,代表這是透過擴散效果的最佳化。應用程式專屬套件為 21.1 KB 並不好壞,但請注意,沒有任何應用程式在發生搖動。讓我們看看應用程式的程式碼,並瞭解如何修正這個問題。
在任何應用程式中,尋找樹林改善機會都必須尋找靜態 import
陳述式。主要元件檔案頂端附近會顯示一行文字,如下所示:
import * as utils from "../../utils/utils";
您可以透過多種方式匯入 ES6 模組,但應特別留意這類模組。這一行程式碼顯示「utils
模組中的「import
」內容,並放入名為 utils
的命名空間中」。這裡最重要的問題是:「這個單元的內容有多少?」
若查看 utils
模組原始碼,會發現其中約有 1,300 行程式碼,
你需要所有項目嗎?請搜尋匯入 utils
模組的主要元件檔案,確認該命名空間的例項數量。
![文字編輯器中的「utils.」搜尋結果螢幕截圖,只顯示 3 筆結果。](https://web.developers.google.cn/static/articles/reduce-javascript-payloads-with-tree-shaking/image/a-screenshot-a-search-a-b9c933e924ee5.png?authuser=19&hl=zh-tw)
utils
命名空間,只會在主要元件檔案中叫用三次。
結果是,utils
命名空間只出現在應用程式中的三個位置,但對於有哪些函式?如果您再次查看主要元件檔案,它似乎只有一個函式,也就是 utils.simpleSort
,可在排序下拉式選單變更時,按照多種條件排序搜尋結果清單:
if (this.state.sortBy === "model") {
// `simpleSort` 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);
}
內含 1,300 個匯出項目的資料行,其中僅使用一個匯出項目。這樣就會傳送許多未使用的 JavaScript。
雖然這個範例應用程式有點複雜,但這並沒有改變本質綜合情境,因為這類情境看起來與您在正式版網頁應用程式中可能遇到的實際最佳化機會相仿。既然您已經找出了利用搖晃的好機會,實際上該怎麼做呢?
避免 Babel 將 ES6 模組轉譯為 CommonJS 模組
Babel 是不可或缺的工具,但它可能會使樹在晃動時較難以察覺。使用 @babel/preset-env
時,Babel「可」將 ES6 模組轉換為更廣泛相容的 CommonJS 模組,也就是 require
而非 import
的模組。
由於樹軸在 CommonJS 模組中很難做到,因此 Webpack 不知道該使用哪些套件時,應從套件中縮減內容。解決方法是將 @babel/preset-env
設為明確保留 ES6 模組。無論是在 babel.config.js
或 package.json
中設定 Babel,都必須額外加入一些額外項目:
// babel.config.js
export default {
presets: [
[
"@babel/preset-env", {
modules: false
}
]
]
}
在 @babel/preset-env
設定中指定 modules: false
會讓 Babel 正常運作,讓 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
修改 fruits
陣列 (超出其範圍) 時,就會產生副作用。
副作用也適用於 ES6 模組,這在樹林中相當重要。如果模組獲取可預測的輸入內容,且不會修改其範圍外任何內容,模組就產生同樣可預測的輸出內容,是未使用時可安全地捨棄的模組。屬於獨立的「模組化」程式碼。也就是「模組」。
如果與 webpack 有關,您可以在專案的 package.json
檔案中指定 "sideEffects": false
,使用提示指定套件和其依附元件沒有副作用:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": false
}
或者,您也可以告訴網路包哪些特定檔案不會產生副作用:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": [
"./src/utils/utils.js"
]
}
在接下來的範例中,任何未指定的檔案均不會產生副作用。如果不想將這個參數加入 package.json
檔案,您也可以透過 module.rules
在 webpack 設定中指定這個標記。
僅匯入必要項目
指示 Babel 單獨保留 ES6 模組後,必須稍微調整 import
語法,才能只納入 utils
模組所需的函式。在本指南範例中,您只需要 simpleSort
函式即可:
import { simpleSort } from "../../utils/utils";
由於只會匯入 simpleSort
,而不是整個 utils
模組,因此 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);
}
這應該就是讓樹軸得以開始運作的一切必要條件。這是在搖動依附元件樹狀結構之前的 webpack 輸出內容:
Asset Size Chunks Chunk Names
js/vendors.16262743.js 37.1 KiB 0 [emitted] vendors
js/main.797ebb8b.js 20.8 KiB 1 [emitted] main
以下是搖樹成功「之後」的輸出內容:
Asset Size Chunks Chunk Names
js/vendors.45ce9b64.js 36.9 KiB 0 [emitted] vendors
js/main.559652be.js 8.46 KiB 1 [emitted] main
雖然兩個套裝組合都有顯示效果,但最有助益的 main
軟體包。透過搖動 utils
模組未使用的部分,main
套件會縮減約 60%。這樣不僅可以縮短指令碼下載時間,也能減少處理時間。
搖晃一些樹吧!
搖晃的效率取決於您的應用程式及其依附元件和架構。點選此按鈕即可體驗這項功能,如果您知道之前尚未設定模組整合器來執行此最佳化作業,嘗試並瞭解它對應用程式的好處。
您可能會發現效能大幅提升,甚至完全無法做到。但只要設定建構系統,在正式版本中利用這項最佳化功能,並且選擇性地匯入您的應用程式需求,您就能主動盡可能地縮減應用程式套件的大小。
特別感謝 Kristofer Baxter、Jason Miller、Addy Osmani、Jeff Posnick、Sam Saccone 和 Philip Walton 提供的寶貴意見,讓本文的品質大幅提升。