Webpack 3一些代碼體積優化方案的小結

前言

以前接手公司一個前端項目,開發了幾個月後愈來愈難以忍受項目結構的混亂和打包體積的臃腫(腳手架和基本功能代碼都是從公司的其餘項目複製過來的),若是不當即進行重構,不可思議之後要怎麼維護各個產品線。因而我挺身而出承擔了項目框架的優化任務,這裏分享一下我在打包體積優化中所研究的成果,通過幾輪的努力,成功的將咱們這個 react+antd+immutable+rxjs的較大項目從打包後的9MB下降到了2.5MB,首屏加載(gzip)從600KB+下降到了200KB,而且基本上將穩定的第三方庫,webpack runtime代碼和業務代碼徹底分離,最低限度減小網站更新時用戶須要加載的代碼量。
廢話很少說,下面詳細說明我所作的每個步驟。前端

1. 優化第三方庫

項目裏對庫的使用較爲混亂,有些庫安裝了但不多用或者根本沒用,可是又在webpack中的vendor入口指定打包了進來,形成體積上的浪費,因此須要仔細評估每一個庫是否必要安裝。
react v16對比react v15,加上react-dom,體積上下降了30%,所以果斷升級。node

2. 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

3. ECharts

項目以前是直接使用的完整版的echarts,而且沒有將echarts組件抽取爲公共chunk,結果致使每一個異步加載的頁面組件,只要用了echarts就會變得碩大無比。
解決方案:在echarts官網定製一份僅包含項目所需圖表類型的閹割版,而且將echarts組件抽取爲異步加載的chunk,這樣就只須要加載一次。
關於如何將組件抽取爲單獨的chunk,能夠用import()語法,或者使用react-loadable這個庫,它能夠直接將react組件包裝成異步組件,並在須要時才進行加載。json

4. 抽取異步加載的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的公共代碼,咱們設置deepChildrentruechildren指的是入口文件的直接子節點,deepChildren指的是所有子節點)。async表示生成一個懶加載的chunk,只有當須要時纔會被加載。
上面只是將第三方庫的公共代碼提取了出來,若是但願把異步chunk當中本身的業務代碼提取出來,則能夠修改minChunks規則,或者再增長一個配置:緩存

new webpack.optimize.CommonsChunkPlugin({
      async: 'async-biz',
      deepChildren: true,
      minChunks: 2,
    }),

5. 並不是每一個路由頁面組件都須要異步加載

項目以前的作法是,每一個路由對應的頁面根組件都須要異步加載,這樣作的結果是打包出了不少個chunk,而有一半的chunkgzip以前體積都不足5KB,浪費請求是一方面,更嚴重的是影響了首屏加載體積。
這是爲何?明明把每一個頁面都異步加載了,怎麼會影響首屏體積呢?其實緣由就是第三步中的async-vendor被首屏加載了,該chunk主要包含了antd組件,gzip以後約爲120KB
對於用戶來講,第一次打開咱們的網站必定是到登陸界面,此時須要徹底加載咱們的首屏代碼,以後有了緩存,除了業務代碼更新須要加載很小的chunk以外,理論上是不須要再下載任何代碼的,所以咱們須要針對登陸界面進行首屏優化。
登陸界面包含了登陸、修改密碼、申請帳號等子路由,以前將這些都打包爲異步chunk,因爲這些界面須要async-vendor當中的某幾個antd組件,所以首屏加載必定會包含async-vendor。拆分async-vendor是一種辦法,可是還要分析到底用了哪些組件,改動業務代碼後又要從新分析,顯得很麻煩,最簡單的作法就是取消登陸相關路由的異步加載,將其打包到main當中,同時只需加載須要的antd組件,所以徹底避免了加載async-vendor,首屏體積獲得了大大下降。babel

6. 分離出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當中。

7. webpack內部優化

這部份內容很簡單,就兩個插件的使用,HashedModuleIdsPluginModuleConcatenationPlugin
默認狀況下,webpack會爲每一個模塊用數字作爲ID,這樣會致使同一個模塊在添加刪除其餘模塊後,ID會發生變化,不利於緩存。爲了解決這個問題,有兩種選擇:NamedModulesPluginHashedModuleIdsPlugin,前者會用模塊的文件路徑做爲模塊名,後者會對路徑進行md5處理,下降了文件體積,相比較而言,應該開發時選擇前者,生產環境選擇後者。
ModuleConcatenationPlugin主要是做用域提高,將全部模塊放在同一個做用域當中,一方面能提升運行速度,另外一方面也能下降文件體積。前提是你的代碼是用es模塊寫的。

8. 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,看來又要研究新的東西了。。。

參考文章:
CommonsChunkPlugin學習小結

相關文章
相關標籤/搜索