- 原文地址:Keep webpack Fast: A Field Guide for Better Build Performance
- 原文做者:Rowan Oulton
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Noah Gao
- 校對者:tvChan,MechanicianW
webpack 是用於打包前端資源的絕佳工具。然而,當運行開始變慢時,開箱即用的生態和大量的第三方工具使得優化變得十分困難。雖然性能不佳是一種常態而不是特例。但也不是沒有辦法來優化,通過幾個小時的調研與試錯,我完成了這樣一份現場指南,可讓咱們在加快構建的道路上學到更多知識。javascript
昔日的構建工具:鏈接提花機的織機。前端
2017 年是 Slack 前端團隊雄心勃勃的一年。通過幾年的快速迭代開發,咱們有很多的技術債務和進行大規模現代化的宏偉計劃。首先,咱們計劃用 React 重寫咱們的 UI 組件,並全面使用上現代 JavaScript 語法。然而在咱們但願這一點可以實現以前,咱們須要一套構建系統來支持這一新的工具星雲。java
到目前爲止,咱們只能依靠文件的簡單鏈接,雖然這一體系已經讓咱們走到了這一步,但顯然它不會讓咱們再更進一步了。 咱們須要一套真正的構建系統。因此,做爲一個具備良好的社區支持、易用性和功能集的強大起點,咱們選擇了 webpack。node
咱們的項目切換到 webpack 的過渡大部分是平穩的。很平穩,直到,它遇到了構建性能問題。咱們的構建花了幾分鐘,而不是幾秒鐘:與咱們曾經習慣的秒級鏈接相差甚遠。Slack 的 Web 團隊在任何一個工做日均可以部署 100 次,因此咱們感受到了構建時間的急劇增加。react
構建性能一直是 webpack 用戶羣的關注重點,儘管核心團隊在過去幾個月裏一直在努力改進,但你仍然能夠採起不少方法來自行改進本身的構建。下面的這些技巧幫助咱們將構建時間縮短了 10 倍,咱們將它們分享出來,但願能幫助到你們。android
在嘗試優化以前,最重要的是瞭解時間在哪裏被浪費掉了。webpack 沒有提供這些信息,但這些必需的信息還能經過其餘的方法來獲得。webpack
Node 自帶了一個能夠用來分析構建的 inspector。若是你不熟悉性能分析,不須要灰心:Google 很努力地解釋了 實現的細節。對 webpack 構建階段的粗略理解在這裏將是很是有益的,儘管他們的文檔 簡要介紹了這一點,但閱讀一些 核心 代碼 是很是有益的。ios
請注意,若是您的構建內容足夠大(好比有數百個模塊或是需時超過一分鐘),則可能須要將分析過程分解爲多個部分,以防止開發人員工具崩潰。git
分析幫助咱們肯定了咱們構建前端的緩慢部分,可是它不適合隨着時間的推移觀察趨勢。咱們但願每次構建都可以報告精確的時序數據,以便咱們能夠看到在每一個昂貴的步驟(轉譯,壓縮和本地化)中花費了多少時間,並肯定咱們的優化是否有效。github
對於咱們來講,大部分的工做不是由 webpack 自己完成的,而是由咱們所依賴的各類加載器和插件完成的。總的來講,這些依賴並無提供精確的時序數據,雖然咱們但願看到 webpack 採用標準化的方式來向第三方報告這種信息,可是與此同時咱們發現咱們必須手動進行一些額外的日誌記錄。
對於加載器來講,這意味着解除咱們的依賴關係。雖然這不適合做爲一個長期策略,可是在咱們進行優化的時候,對於咱們辨認出過程當中緩慢的部分是很是有用的。另外一方面,插件更容易分析。
插件將本身附加到與構建的不一樣階段相關的 事件 上。經過測量這些階段的持續時間,咱們能夠粗略的測量咱們插件的執行時間。
UglifyJSPlugin 是一個典型的測量插件,這種技術是有效的,由於其大部分工做是在 optimize-chunk-assets 階段。下面是一個簡單的插件例程:
let CrudeTimingPlugin = function() {};
CrudeTimingPlugin.prototype.apply = function(compiler) {
compiler.plugin('compilation', (compilation) => {
let startOptimizePhase;
compilation.plugin('optimize-chunk-assets', (chunks, callback) => {
// 使用粗略測量壓縮時間的方法。
// UglifyJSPlugin 在這個編譯階段完成所有工做,
// 因此咱們計算整個階段的時間。
startOptimizePhase = Date.now();
// 對於異步階段,不要忘記調用回調函數
callback();
});
compilation.plugin('after-optimize-chunk-assets', () => {
const optimizePhaseDuration = Date.now() - startOptimizePhase;
console.log(`optimize-chunk-asset phase duration: ${optimizePhaseDuration}`);
});
});
};
module.exports = CrudeTimingPlugin;
複製代碼
上面的例子目的是粗略地測量 UglifyJSPlugin 的執行時間差。請注意瞭解插件將在哪些階段執行,由於可能有重疊。
把它添加到你的插件列表裏,在 UglifyJS 以前,就像這樣:
const CrudeTimingPlugin = require('./crude-timing-plugin');
module.exports = {
plugins: [
new CrudeTimingPlugin(),
new UglifyJSPlugin(),
]
};
複製代碼
這些信息的價值大大超過了獲取它的成本,一旦你明白了時間花在了哪裏,就可以有效地減小花費的時間。
webpack 的不少工做自己就是並行的。經過把工做擴展到儘量多的處理器上來得到巨大的效果,若是你有多餘的 CPU 核心可「燒」,如今是「燒掉它」的時候了。
幸運的是,有一堆以此爲目的打造的軟件包:
注意,拉起新線程有一個不小的成本。建議只在消耗較大的操做中,基於你以前的分析,靈活地應用它們。
當咱們的 webpack 測量實現完成時,咱們意識到在幾個地方作了沒必要要的工做。砍掉這些地方爲咱們節省了大量的時間:
壓縮是一個巨大的時間沉澱 —— 佔據咱們三分之一到一半的構建時間。咱們評估了不一樣的工具,從 Butternut 到 babel-minify,結果卻發現 UglifyJS 在並行配置下是最快的。
然而,對咱們來講,關於要處理的性能問題相關的核心信息 被埋在做者的長篇大論之下
同你們認爲的不一樣,對於大多數 JavaScript 來講,空白的去除和符號的改變可以壓縮代碼的 95%,是主要代碼壓縮的核心,而不是精心設計的代碼轉換。人們能夠簡單地禁用壓縮加速 Uglify 構建 3 至 4 倍。
咱們試了一下,結果使人咋舌。就像承諾的那樣,壓縮速度是原來的 3 倍,並且咱們生成的打包文件大小几乎沒有增加。不過 React 用戶以這種方式禁用壓縮應該警戒一個警告:detection methods 被 react-devtools 用來報告你正在使用 React 的開發版本。通過一些試錯,咱們發現如下配置解決了這個問題:
new UglifyJsPlugin({
uglifyOptions: {
compress: {
arrows: false,
booleans: false,
cascade: false,
collapse_vars: false,
comparisons: false,
computed_props: false,
hoist_funs: false,
hoist_props: false,
hoist_vars: false,
if_return: false,
inline: false,
join_vars: false,
keep_infinity: true,
loops: false,
negate_iife: false,
properties: false,
reduce_funcs: false,
reduce_vars: false,
sequences: false,
side_effects: false,
switches: false,
top_retain: false,
toplevel: false,
typeofs: false,
unused: false,
// 除非聲明瞭正在使用生產版本的react-devtools,
// 不然關閉全部類型的壓縮。
conditionals: true,
dead_code: true,
evaluate: true,
},
mangle: true,
},
}),
複製代碼
注意:此配置適用於 UglifyJS webpack 插件的 1.1.2 版本。
檢測變量根據版本而不一樣,React 16用戶可能單獨使用_compress:false_。
一般優先考慮最終發送給用戶的字節數,因此請注意在工程團隊和下載應用程序的用戶之間取得平衡。
開發中須要找到並進入多個相同代碼的包是很常見的事。當這種狀況發生時,壓縮器的工做將沒必要要地增長。 咱們把打包經過 webpack Bundle Analyzer 和 Bundle Buddy 這兩部顯微鏡找到重複的項,並將其用 webpack 的 CommonsChunkPlugin 分紅共享塊。
webpack 會在查找依賴關係的同時,將每一個 JavaScript 文件解析爲 語法樹。這個過程是很昂貴的,因此若是你肯定一個文件(或一組文件)永遠不會使用 import,require 或者 define 語句,你能夠告訴 webpack 在這個過程當中排除它們。以這種方式跳過大型庫能夠大幅提升效率。有關更多詳細信息,請參見 noParse 選項。
經過相似的方式,你能夠從加載器 排除 文件,許多插件提供 相似的選項。這能夠實在的提升工具的性能,例如也依靠語法樹來完成自身工做的轉譯器和壓縮器。在 Slack 中,咱們只編譯咱們確認使用了 ES6 特性的代碼,而且忽略不直接提供給客戶的代碼的壓縮。
DllPlugin 將容許你在後面的階段剝離預先構建好的包供 webpack 使用,很是適合像 Vendor 庫這樣的大型,較少移動的依賴項。雖然它傳統上是一個須要大量配置的插件,可是 autodll-webpack-plugin 爲更簡單的實現鋪平了道路,值得一看。
webpack 爲依賴關係樹中的每一個模塊分配一個 ID。隨着新模塊的添加以及其餘模塊的移除,樹會發生變化,同時也會改變其中每一個模塊的 ID。這些 ID 被置入每一個 webpack 發出的文件中,而高級別的模塊混合(譯者注:應指交叉依賴,npm 一直以來的的一大嚴重問題)可能致使沒必要要的重建。 經過使用 records 來防止這種狀況,在構建之間穩定您的模塊ID。
在 Slack,每次發佈新版本時,咱們都會使用哈希文件名來緩存破解。打開瀏覽器開發人員工具的「網絡」選項卡,您將看到「_application.d4920286de51402132dc.min.js」文件的請求。這種技術對於緩存控制來講是很是棒的,可是這也意味着 webpack 沒法在不借助摘要的狀況下將模塊映射到相應的文件名。
摘要是模塊 ID 到哈希的簡單映射,當 異步導入模塊時,webpack 將用它來解析文件名:
{
0: "d4920286de51402132dc", /* ← 爲應用打包而生成的哈希值 */
1: "29a3cf9344f1503c9f8f",
2: "e22b11ab6e327c7da035",
/* .. 等等等 ... */
}
複製代碼
默認狀況下,webpack 將在它添加到每一個打包文件頂部的樣板代碼中包含這個摘要。然而這是有問題的,由於每次添加或刪除模塊時摘要都必須更新 —— 這種狀況咱們天天都會發生。每當摘要發生變化時,咱們不只須要等待全部打包文件的重建,並且還要破壞緩存,迫使咱們的客戶從新下載它們。
僅僅保持模塊ID穩定是不夠的。咱們須要將模塊摘要徹底提取到一個單獨的文件中;在咱們或是咱們的客戶沒有花費重建和從新下載任何東西的成本的狀況下,就可以按期改變。因此咱們用CommonsChunk插件建立了一個 manifest文件。這大大減小了重建的頻率,並且還讓咱們只發送了一個 webpack 的樣板代碼的副本。
源地圖(Source maps)是調試時用到的關鍵工具,可是生成它們將花費必定時間,改動 webpack 的 開發工具菜單選項 並選擇一個最合適本身的調試風格。 cheap-source-map 方案在構建性能和可調試性間取得了不錯的平衡。
咱們的部署節奏很快,這意味着當前的構建和以前的之間一般只有很小的差別。隨着在正確的地方被緩存,咱們能夠加速大部分 webpack 原本會作的工做。
咱們使用 cache-loader 來緩存結果(babel-loader 的用戶一般會優先選擇使用它的 內建緩存,UglifyJSPlugin 的 內建緩存,以及加入了 HardSourceWebpackPlugin。
webpack 所作的不少工做都在加載器/插件執行以外,並且大部分工做都會遵循傳統避開緩存。爲了解決這個問題,咱們引入了一個插件 HardSourceWebpackPlugin,用於緩存 webpack 內部模塊處理的中間結果。
爲此,咱們必須仔細列舉可能須要緩存的全部外部因素,並完全地進行測試。在咱們的例子中包括:轉移,CDN 資源路徑和依賴版本。這不是個輕鬆地差事,但結果是值得的 —— 啓動緩存後,咱們的熱構建快了 20 秒。
最後要注意的是,每當程序包依賴性發生變化時,請記住清除緩存 - 可使用 npm postinstall script 自動執行。一個陳舊、不兼容的緩存可能會以新的和有趣的方式對你的構建形成嚴重破壞。
在 webpack 生態系統中,保持最新狀態是值得的。核心團隊近期已經作了不少工做來提升構建速度,若是你沒有使用最新版本的依賴項,你可能會錯過大量的性能提高。 當咱們從 webpack 3.0 升級到 3.4 時,咱們發現加速了幾十秒鐘,而咱們徹底沒有改變配置,而且這樣的改進還在繼續。
按期升級並跟上前面提到的如並行性等新功能的更新。在 Slack ,咱們盡咱們所能地留意 Github 上的發佈,webpack團隊博客, babel團隊博客以及其餘有關他們工做的博客。
不要忘記讓你的 Node 保持在最新的版本 — 軟件包不是惟一的改進途徑。
當一天結束的時候,你的構建必須在某個地方運行,而且要在某個東西上運行。 若是最終的構建是在史前級的設備上進行的話,那麼對總體構建性能,即使進行了最優秀的優化,都會產生很大的影響。
當咱們的任務剛開始進行時,咱們的構建服務器是 Amazon EC2 家族的成員,C3。 經過將實例類型更新到 C4 產品(處理器更快,更強大),隨着代碼庫的增加,咱們看到了構建時間和可用於擴展的並行能力相關選項的顯著改進。 用戶一般擔憂的從實例支持的機器到 EBS 的過渡過程不須要感到絕望:webpack 積極地緩存文件操做,咱們沒有發現遷移到 EBS 後性能存在下降現象。
若是它在您的能力(和預算)範圍內,那麼請評估更好的硬件和基準,以找到最佳的配置。
像 webpack 這樣的基礎設施項目幾乎都出奇的窮; 不管是時間仍是金錢,對您使用的工具作出貢獻將爲您和社區中的其餘人改善這一工具的生態系統。Slack 最近爲 webpack 項目作了捐贈,以確保團隊可以繼續工做,咱們鼓勵其餘人也這樣作。
貢獻也能夠經過反饋的形式進行。做者每每熱衷於聽到他們的用戶提供的更多信息,瞭解他們須要在哪裏花費最多的精力,並且 webpack 甚至鼓勵用戶 對核心團隊的優先事項投票。 若是你關心構建性能,或者你已經有了改進的想法,那就讓你的聲音被你們聽到吧。
webpack 是一個夢幻般的,多功能工具,不須要花費天價。這些技術幫助咱們將建造時間的中位數從 170 秒縮短到了 17 秒,儘管他們爲咱們的工程師們提升了部署經驗,但他們並非一個已經十分完善的項目。若是您對如何進一步提升構建性能有任何想法,咱們很樂意聽取您的意見。固然,若是你喜歡解決這些問題 來和咱們一塊兒工做吧!
很是感謝 Mark Christian, Mioi Hanaoka, Anuj Nair, Michael 「Z」 Goddard, Sean Larkin and, of course, Tobias Koppers 對這篇文章和 webpack 項目作出的貢獻。
感謝 Matt Haughey 的支持。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。