以前接手公司一個前端項目,開發了幾個月後愈來愈難以忍受項目結構的混亂和打包體積的臃腫(腳手架和基本功能代碼都是從公司的其餘項目複製過來的),若是不當即進行重構,不可思議之後要怎麼維護各個產品線。因而我挺身而出承擔了項目框架的優化任務,這裏分享一下我在打包體積優化中所研究的成果,通過幾輪的努力,成功的將咱們這個 react
+antd
+immutable
+rxjs
的較大項目從打包後的9MB
下降到了2.5MB
,首屏加載(gzip)從600KB+
下降到了200KB
,而且基本上將穩定的第三方庫,webpack runtime
代碼和業務代碼徹底分離,最低限度減小網站更新時用戶須要加載的代碼量。
廢話很少說,下面詳細說明我所作的每個步驟。前端
項目裏對庫的使用較爲混亂,有些庫安裝了但不多用或者根本沒用,可是又在webpack
中的vendor
入口指定打包了進來,形成體積上的浪費,因此須要仔細評估每一個庫是否必要安裝。react v16
對比react v15
,加上react-dom
,體積上下降了30%,所以果斷升級。node
moment.js
分析完stats.json
後,發現的第一個問題就是moment
很大,具體緣由webpack
是把全部的locale
文件打包了進來。咱們的項目不須要多語言,所以咱們可使用ContextReplacementPlugin
插件來捨棄中文以外的其餘語言文件:react
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /zh-cn/)
去除掉locale
後,又發現了另外一個問題:依賴分析顯示,個人項目裏打包了兩份moment
,一份es module
版的,一份umd
版的。通過一番排查後發現,使用import 'moment'
導入的會加載es module
版的,這是webpack
配置的mainFields
決定的,可是在locale
中的語言文件中,它會用相對路徑導入umd
版的moment
,這就致使個人項目裏出現了兩份moment
。爲了統一版本,咱們將moment
設置爲一個別名並指向umd
版:webpack
alias: { 'moment$': path.resolve('node_modules/moment/moment'), }, // $表示絕對匹配
另外還有一個庫dayjs
值得一提,其API
基本與moment
一致,可是體積僅爲幾KB,不知道antd
會不會加入對dayjs
的支持。web
ECharts
項目以前是直接使用的完整版的echarts
,而且沒有將echarts
組件抽取爲公共chunk
,結果致使每一個異步加載的頁面組件,只要用了echarts
就會變得碩大無比。
解決方案:在echarts
官網定製一份僅包含項目所需圖表類型的閹割版,而且將echarts
組件抽取爲異步加載的chunk
,這樣就只須要加載一次。
關於如何將組件抽取爲單獨的chunk
,能夠用import()
語法,或者使用react-loadable
這個庫,它能夠直接將react
組件包裝成異步組件,並在須要時才進行加載。json
chunk
中的公共代碼上面的步驟抽取echarts
就是指的抽取異步chunk
中的公共代碼,除了echarts
以外還有不少大致積的公共代碼,例如各類antd
的組件以及其依賴的底層組件rc-components
,這部分也是咱們要提取出來的。咱們不必將每一個antd
組件包裝爲異步組件,這裏只須要配置一下CommonsChunkPlugin
就能夠了:segmentfault
new webpack.optimize.CommonsChunkPlugin({ async: 'async-vendor', deepChildren: true, minChunks: (module) => { return /node_modules/.test(module.context); }, }),
在沒有將children
設爲true
時,CommonsChunkPlugin
會從入口文件(entry
)提取公共代碼,這時就不會對異步加載的chunk
起做用。所以爲了提取異步chunk
的公共代碼,咱們設置deepChildren
爲true
(children
指的是入口文件的直接子節點,deepChildren
指的是所有子節點)。async
表示生成一個懶加載的chunk
,只有當須要時纔會被加載。
上面只是將第三方庫的公共代碼提取了出來,若是但願把異步chunk
當中本身的業務代碼提取出來,則能夠修改minChunks
規則,或者再增長一個配置:緩存
new webpack.optimize.CommonsChunkPlugin({ async: 'async-biz', deepChildren: true, minChunks: 2, }),
項目以前的作法是,每一個路由對應的頁面根組件都須要異步加載,這樣作的結果是打包出了不少個chunk
,而有一半的chunk
在gzip
以前體積都不足5KB
,浪費請求是一方面,更嚴重的是影響了首屏加載體積。
這是爲何?明明把每一個頁面都異步加載了,怎麼會影響首屏體積呢?其實緣由就是第三步中的async-vendor
被首屏加載了,該chunk
主要包含了antd
組件,gzip
以後約爲120KB
。
對於用戶來講,第一次打開咱們的網站必定是到登陸界面,此時須要徹底加載咱們的首屏代碼,以後有了緩存,除了業務代碼更新須要加載很小的chunk
以外,理論上是不須要再下載任何代碼的,所以咱們須要針對登陸界面進行首屏優化。
登陸界面包含了登陸、修改密碼、申請帳號等子路由,以前將這些都打包爲異步chunk
,因爲這些界面須要async-vendor
當中的某幾個antd
組件,所以首屏加載必定會包含async-vendor
。拆分async-vendor
是一種辦法,可是還要分析到底用了哪些組件,改動業務代碼後又要從新分析,顯得很麻煩,最簡單的作法就是取消登陸相關路由的異步加載,將其打包到main
當中,同時只需加載須要的antd
組件,所以徹底避免了加載async-vendor
,首屏體積獲得了大大下降。babel
webpack runtime
代碼webpack
在客戶端運行時會首先加載webpack
相關的代碼,例如require
函數等,這部分代碼會隨着每次修改業務代碼後發生變化,緣由是這裏面會包含chunk id
等容易變化的信息。若是不抽取出來將會被打包在vendor
當中,致使vendor
每次都要被用戶從新加載,vendor
也失去了它的意義。分離的配置很簡單:antd
new webpack.optimize.CommonsChunkPlugin({ name: 'manifest', minChunks: Infinity, }),
minChunks: Infinity
表示建立一個什麼都沒有的chunk
,由於不會有任何模塊被無限次引用過,這樣webpack runtime
代碼就會被CommonsChunkPlugin
放入這個最後的chunk
當中。
webpack
內部優化這部份內容很簡單,就兩個插件的使用,HashedModuleIdsPlugin
和ModuleConcatenationPlugin
。
默認狀況下,webpack
會爲每一個模塊用數字作爲ID
,這樣會致使同一個模塊在添加刪除其餘模塊後,ID
會發生變化,不利於緩存。爲了解決這個問題,有兩種選擇:NamedModulesPlugin
和HashedModuleIdsPlugin
,前者會用模塊的文件路徑做爲模塊名,後者會對路徑進行md5
處理,下降了文件體積,相比較而言,應該開發時選擇前者,生產環境選擇後者。ModuleConcatenationPlugin
主要是做用域提高,將全部模塊放在同一個做用域當中,一方面能提升運行速度,另外一方面也能下降文件體積。前提是你的代碼是用es
模塊寫的。
babel-polyfill
polyfill
也是體積很大的一部分,可是又不得不加載,關於這部分的優化能夠參考這篇文章,ES6和Babel你不知道的事兒。還有一種方法是使用polyfill.io
,這個解決思路我的以爲很不錯,可是還不敢在生產環境用,先觀望觀望。
以上內容是我這些天找資料研究的結果,總的來講打包體積算是獲得了有效控制,關於chunk
的打包配置以下:
entry: { main: path.join(process.cwd(), 'src/index.js'), vendor: [ 'babel-polyfill', 'immutable', 'moment', 'react', 'react-dom' ... ], }, output: { filename: '[name].[chunkhash].js', chunkFilename: '[name].[chunkhash].chunk.js', }, plugins: [ new webpack.HashedModuleIdsPlugin(), new webpack.optimize.ModuleConcatenationPlugin(), new webpack.optimize.CommonsChunkPlugin({ async: 'async-vendor', deepChildren: true, minChunks: (module) => { return /node_modules/.test(module.context); }, }), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', }), new webpack.optimize.CommonsChunkPlugin({ name: 'manifest', minChunks: Infinity, }), ]
webpack 4
已經出了,再也沒有CommonsChunkPlugin
了,取而代之的是SplitChunksPlugin
,看來又要研究新的東西了。。。