Webpack 4进阶--从前的日色变得慢 ,一下午只够打一次包
從前的日色變得慢,車,馬,郵件都慢,一生只夠愛一個人 -- 《從前慢》
近期在團隊項目里把Webpack升級到4.4.1,過程中發(fā)現(xiàn)現(xiàn)存的升級文檔十分有限,踩了不少坑,好在升級之后提升還算顯著,production場景下第三方依賴打包速度提升76%,development場景下本地服務(wù)首次啟動提升效果約46%,再次啟動提升效果上升至63%。這里將這次升級過程中的點滴分享出來,希望對大家有所幫助。
理論部分
Webpack 4 發(fā)布之后,議論最多的兩大特性,其一是零配置,其二是速度快(號稱提速上限98%)。聽起來十分美妙,在實地測試之前,首先從理論上分析一下可能性。
零配置
一言以蔽之,約定優(yōu)于配置。通過mode屬性將開發(fā)/生產(chǎn)(development/production)環(huán)境中常用的功能設(shè)置好默認值,用戶即來即用。
打包速度快
Optimization
Webpack 4取消了四個常用的用于性能優(yōu)化的plugin(UglifyjsWebpackPlugin,CommonsChunkPlugin,ModuleConcatenationPlugin,NoEmitOnErrorsPlugin),轉(zhuǎn)而提供了一個名為optimization的配置項,用于接手以上四位的工作。
注:UglifyjsWebpackPlugin并不執(zhí)行tree shaking操作,這里為了介紹sideEffects,故而將關(guān)系緊密的兩者放在一起介紹了廢棄插件:UglifyjsWebpackPlugin
新增屬性:sideEffects,minimize等
- Tree Shaking
Tree shaking一直是一個美麗而遙不可及的話題。影響tree shaking的根本原因在于side effects(副作用),其中最廣為人知的一條side effect就是動態(tài)引入依賴的問題。得益于ES6的模塊化實現(xiàn)思路,所有的依賴必須位于文件頂部,靜態(tài)引入(然而import()的出現(xiàn)打破了這個規(guī)則),Webpack可以在繪制依賴圖的時候進行靜態(tài)分析,從而將真正被引用的exports添加到bundle文件中,減少打包體積。然而很多熱度較高的第三方庫為了考慮兼容性往往采用UMD實現(xiàn),而其所支持的動態(tài)引入依賴的功能則導(dǎo)致真實的依賴圖可能要到運行時才能確定,使得靜態(tài)分析難以發(fā)揮真正威力,tree shaking采用了保守策略,導(dǎo)致我們發(fā)現(xiàn)沒有被用到的方法依然出現(xiàn)在了bundle文件中。
一個好消息是許多第三方庫相繼推出了es版,配合tree-shaking食用,口感更佳,這也是官方號稱提速98%的重要前提之一(冷漠臉)。壞消息是ES6其實也提供import()方法支持動態(tài)引入依賴,所以以下寫法其實也是完全行的通的。。。還記得那些年我們追過的沈佳宜說過的話么,“人生本來就有很多事情是徒勞無功的啊”。
if(Math.random() > 0.5) {import('./a.js').then(() => {...}) } else {import('./b.js').then(() => {...}) }除此以外,為了防止用戶不小心修改輸出元素的屬性,有些庫會將最終的輸出元素用Object.freeze方法包裹起來,這也屬于side effects之一,同樣也會對tree shaking產(chǎn)生影響。
回到Webpack 4,官方提供了sideEffects屬性,通過將其設(shè)置為false,可以主動標(biāo)識該類庫中的文件只執(zhí)行簡單輸出,并沒有執(zhí)行其他操作,可以放心shaking。除了可以減小bundle文件的體積,同時也能夠提升打包速度。為了檢查side effects,Webpack需要在打包的時候?qū)⑺械奈募?zhí)行一遍。而在設(shè)置sideEffects之后,則可以跳過執(zhí)行那些未被引用的文件,畢竟已經(jīng)明確標(biāo)識了“我是平民”。因此對于一些我們自己開發(fā)的庫,設(shè)置sideEffects為false大有裨益。
- Minimize
Minimize屬性就沒啥可多說的了,混淆壓縮文件。
廢棄插件:ModuleConcatenationPlugin
新增屬性:concatenateModules
//開啟前 [/* 0 */function(module, exports, require) {var module_a = require(1)console.log(module_a['default'])}/* 1 */function(module, exports, require) {exports['default'] = 'module A'} ]//開啟后 [function(module, exports, require) {var module_a_defaultExport = 'module A'console.log(module_a_defaultExport)} ]concatenateModules開啟之后,可以看出bundle文件中的函數(shù)聲明變少了,因而可以帶來的好處,其一,文件的體積比之前更小了,其二,運行代碼時創(chuàng)建的函數(shù)作用域變少了,開銷也隨之變少了。不過scope hoisting的效果同樣也依賴于靜態(tài)分析,無奈命不由我。
廢棄插件:CommonsChunkPlugin
新增屬性:splitChunks,runtimeChunk, occurrenceOrder等
- splitChunks
splitChunks在Webpack 4里被用于取代我們熟悉CommonsChunkPlugin。讀到這里不知道你有沒有發(fā)現(xiàn)其中的端倪,這是否意味著DllPlugin和CommonsChunkPlugin(splitChunks)可以共存了呢?
在Webpack 4之前,兩者并不能一起使用,原因有二
- 一個相對沒那么重要的原因是DllPlugin服務(wù)的目標(biāo)場景是develop環(huán)境,因為第三方依賴(輸出文件暫稱為vendors)的變更頻率較低,故而在每次啟動本地服務(wù)或者rebuild的時候?qū)⒌谌揭蕾囍匦麓虬淮螌嶋H上是一種浪費。通過DllPlugin,將第三方依賴的打包過程從業(yè)務(wù)代碼的打包過程中獨立出來,可以大大縮短develop環(huán)境下的啟動時間。同時通過設(shè)置hash值,也可以充分的利用瀏覽器對這部分文件的緩存,提升加載效率。而在對加載效率更為苛刻的production環(huán)境,DllPlugin打包出的文件則稍顯笨重,很多重復(fù)的內(nèi)容被多次打包進了bundle文件。在這種場景下,CommonsChunkPlugin被視為更好的選擇,因為我們不需要為打包時間操心過多,加載效率是我們唯一需要關(guān)注的內(nèi)容。所以在webpack的開發(fā)者看來,這兩者如同“I have an apple,I have a pen,Ah~~ Apple pen”一樣,實際上并不存在什么交集。
- 因此也引出了二者不兼容更為重要的第二個原因,沒人實現(xiàn)
這塊功能實際上通過CommonsChunkPlugin設(shè)置兩個entry point也可以實現(xiàn),一個作為業(yè)務(wù)代碼的入口,一個作為vendors的入口。不過存在兩個問題,第一個問題是,盡管vendors被單獨設(shè)置了entry point,但是在每次啟動本地服務(wù)的時候,盡管打包的結(jié)果不變,hash值不變,瀏覽器的緩存文件也被充分利用了,它的打包過程依然會執(zhí)行,所以啟動時間并不會縮短,第二個問題是,許多人在使用CommonsChunkPlugin的時候并沒有注意到Webpack會將runtime一起打包進vendors文件,所以每次啟動的時候,盡管你并沒有修改任何第三方依賴,但是vendors文件的hash值卻變了,導(dǎo)致瀏覽器緩存實際上并沒有被利用起來。要解決這個問題,需要配置CommonsChunkPlugin將runtime單獨打包成一個文件。
然而到了Webpack 4,在CommonsChunkPlugin變成splitChunks之后,出于某些未知的原因,兩者兼容性的問題被解決了。。。Happy coding。
- runtimeChunk
runtimeChunk之所以被單獨設(shè)置為一個配置項,應(yīng)該就是為了主動幫助用戶避免上文所述的問題吧。
- occurrenceOrder
occurrenceOrder應(yīng)用的場景是如果不手動設(shè)置chunk的名字,而采用默認值的話,Webpack將會用更短的名字去命名引用頻度更高的chunk。
- noEmitOnErrors
廢棄插件:NoEmitOnErrorsPlugin
新增屬性:noEmitOnErrors
noEmitOnErrors在編譯出現(xiàn)錯誤時,用來跳過輸出階段。
New Plugin
Webpack 4同時實現(xiàn)了一套新的plugin機制,與性能相關(guān)的改進點是消除了對arguments的濫用。如同我們推崇開發(fā)時定義類型,從而可以避免JIT過程中產(chǎn)生過多的重載函數(shù),以及降低重新編譯的概率。
實踐部分
講了這么多,最后分享一下我的實操經(jīng)歷。Webpack 4為用戶描繪的場景固然美好,然而帶來便利的同時也給開發(fā)者留下了不少麻煩。首當(dāng)其沖的就是兼容性的問題,很多我們常用的loader,plugin尚未對這次升級做好準(zhǔn)備,找到合適的替代工具以及積極改造自研的工具將成為升級過程中一場重要戰(zhàn)役。接下來我會針對在這次項目升級中我所遇到的兼容性問題以及最終采用的解決方案做一個總結(jié),常規(guī)的Webpack 4配置可以在官方demo 中找到答案。
Nothing special,主要還是一個分類問題,如何識別存在公共依賴的第三方依賴,并將其分配到不同的entry中。例如antd和react都依賴了react,則應(yīng)該將兩者分配到不同的entry中。以及如何均勻的分配依賴到不同的entry中,使得打包之后的每個entry大小相近。可以說十分考驗一名配置工程師的功力和對源碼庫的了解程度。
- ForkTsCheckerWebpackPlugin用于新建進程執(zhí)行類型檢查,為此你需要關(guān)閉ts-loader自身的類型檢查功能,即設(shè)置transpileOnly為true。
- thread-loader允許新建一個worker進程去分擔(dān)一些昂貴的loader操作;cache-loader則可以將loader的運行結(jié)果緩存在本地。然而兩者同時也會帶來額外的開銷(進程管理,I/O操作),自行評估后使用。
最后秀一下數(shù)據(jù)吧
在展示最終結(jié)果之前需要聲明的一點是,由于升級Webpack的同時,還解決了諸多兼容性問題,所以最終結(jié)果的表現(xiàn)無論優(yōu)劣,都不僅僅是Webpack的功過,loader以及plugin替換帶來的性能影響同樣不可忽略。至于如何到達提速98%,如果所有依賴全部更新成為es版本的話。。。
DllPlugin CommonsChunkPlugin對第三方依賴打包場景(production場景) Webpack 3.8.1的打包時長為57411ms,Webpack 4的打包時長為13959ms,提升效果約76%,詳情如下圖所示。
本地啟動(development場景) Webpack 3.8.1的啟動時長(僅包含業(yè)務(wù)代碼打包過程)為42890ms,Webpack 4的首次啟動(cache文件尚未產(chǎn)生)時長為23017ms,Webpack 4的再次啟動(cache文件已經(jīng)存在,并非watch模式下的rebuild場景)時長為15827ms,首次啟動提升效果約46%,再次啟動提升效果上升至63%,詳情如下圖所示。
結(jié)束語
在不糾結(jié)究竟是Webpack還是替換loader&plugin的功勞,以及升級過程中遭遇的懵逼,躁郁,崩潰的情況下,這次升級還是為項目帶來了正反饋。如果你也是一名追求極致開發(fā)體驗的配置工程師的話,這次Webpack升級還是值得嘗試的。最后希望文章中的內(nèi)容能夠有所幫助。
總結(jié)
以上是生活随笔為你收集整理的Webpack 4进阶--从前的日色变得慢 ,一下午只够打一次包的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于React的表单开发的分析(上)
- 下一篇: 【干货】十分钟读懂浏览器渲染流程