webpack打包分析與性能優化

webpack打包分析與性能優化

背景

在去年年底參與的一個項目中,項目技術棧使用react+es6+ant-design+webpack+babel,生產環境全量構建將近三分鐘,項目業務模塊多達數百個,項目依賴數千個,而且該項目協同先後端開發人員較多,提升webpack 構建效率,成爲了改善團隊開發效率的關鍵之一。javascript

下面我將在項目中遇到的問題和技術方案沉澱出來與你們作個分享css

從項目自身出發

咱們的項目是將js分離,不一樣頁面加載不一樣的js。然而分析webpack打包過程並針對性提出優化方案是一個比較繁瑣的過程,首先咱們須要知道webpack 打包的流程,從而找出時間消耗比較長的步驟,進而逐步進行優化。java

在優化前,咱們須要找出性能瓶頸在哪,代碼組織是否合理,優化相關配置,從而提高webpack構建速度。node

1.使用yarn而不是npmreact

因爲項目使用npm安裝包,容易致使在多關聯依賴關係中,極可能某個庫在指定依賴時沒有指定版本號,進而致使不一樣設備上拉到的package版本不一。yarn無論安裝順序如何,相同的依賴關係將以相同的方式安裝在任何機器上。當關聯依賴中包括對某個軟件包的重複引用,在實際安裝時將盡可能避免重複的建立。yarn不只能夠緩存它安裝過的包,並且安裝速度快,使用yarn無疑能夠很大程度改善工做流和工做效率webpack

2.刪除沒有使用的依賴git

不少時候,咱們因爲項目人員變更比較大,參與項目的人也比較多,在分析項目時,我發現了一些問題,諸如:有些文件引入進來的庫沒有被使用到也沒有及時刪除,例如:es6

import a from 'abc';

在業務中並無使用到a 模塊,但webpack 會針對該import 進行打包一遍,這無疑形成了性能的浪費。github

webpack打包分析

1.打包過程分析web

咱們知道,webpack 在打包過程當中會針對不一樣的資源類型使用不一樣的loader處理,而後將全部靜態資源整合到一個bundle裏,以實現全部靜態資源的加載。webpack最初的主要目的是在瀏覽器端複用符合CommonJS規範的代碼模塊,而CommonJS模塊每次修改都須要從新構建(rebuild)後才能在瀏覽器端使用。

那麼, webpack是如何進行資源的打包的呢?總結以下:

  • 對於單入口文件,每一個入口文件把本身所依賴的資源所有打包到一塊兒,即便一個資源循環加載的話,也只會打包一份
  • 對於多入口文件的狀況,分別獨立執行單個入口的狀況,每一個入口文件各不相干

咱們的項目使用的就是多入口文件。在入口文件中,webpack會對每一個資源文件進行配置一個id,即便屢次加載,它的id也是同樣的,所以只會打包一次。

實例以下:
main.js引用了chunk一、chunk2,chunk1又引用了chunk2,打包後:bundle.js:

...省略webpack生成代碼
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {

    __webpack_require__(1);//webpack分配的id
    __webpack_require__(2);

/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {
    //chunk1.js文件
    __webpack_require__(2);
    var chunk1=1;
    exports.chunk1=chunk1;

/***/ },
/* 2 */
/***/ function(module, exports) {
    //chunk2.js文件
    var chunk2=1;
    exports.chunk2=chunk2;

/***/ }
/******/ ]);

2.如何定位webpack打包速度慢的緣由

咱們首先須要定位webpack打包速度慢的緣由,才能因地制宜採起合適的方案。我麼能夠在終端中輸入:

$ webpack --profile --json > stats.json

而後將輸出的json文件到以下兩個網站進行分析

這兩個網站能夠將構建後的組成用可視化的方式呈現出來,可讓你清楚的看到模塊的組成部分,以及在項目中可能存在的多版本引用的問題,對於分析項目依賴有很大的幫助

優化方案與思路

針對webpack構建大規模應用的優化每每比較複雜,咱們須要抽絲剝繭,從性能提高點着手,可能沒有一套通用的方案,但大致上的思路是通用的,核心思路可能包括但不限於以下:

1):拆包,限制構建範圍,減小資源搜索時間,無關資源不要參與構建

2):使用增量構建而不是全量構建

3):從webpack存在的不足出發,優化不足,提高效率

webpack打包優化

1.減少打包文件體積

webpack+react的項目打包出來的文件常常動則幾百kb甚至上兆,究其緣由有:

  • import css文件的時候,會直接做爲模塊一併打包到js文件中
  • 全部js模塊 + 依賴都會打包到一個文件
  • React、ReactDOM文件過大

針對第一種狀況,咱們可使用 extract-text-webpack-plugin,但缺點是會產生更長時間的編譯,也沒有HMR,還會增長額外的HTTP請求。對於css文件不是很大的狀況最好仍是不要使用該插件。

針對第二種狀況,咱們能夠經過提取公共代碼塊,這也是比較廣泛的作法:

new webpack.optimize.CommonsChunkPlugin('common.js');

經過這種方法,咱們能夠有效減小不一樣入口文件之間重疊的代碼,對於非單頁應用來講很是重要。

針對第三種狀況,咱們能夠把React、ReactDOM緩存起來:

entry: {
        vendor: ['react', 'react-dom']
    },
    new webpack.optimize.CommonsChunkPlugin('vendor','common.js'),

咱們在開發環境使用react的開發版本,這裏包含不少註釋,警告等等,部署線上的時候能夠經過 webpack.DefinePlugin 來切換生產版本。

固然,咱們還能夠將React 直接放到CDN上,以此來減小體積。

2.代碼壓縮

webpack提供的UglifyJS插件因爲採用單線程壓縮,速度很慢 ,
webpack-parallel-uglify-plugin插件能夠並行運行UglifyJS插件,這能夠有效減小構建時間,固然,該插件應用於生產環境而非開發環境,配置以下:

var ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
new ParallelUglifyPlugin({
   cacheDir: '.cache/',
   uglifyJS:{
     output: {
       comments: false
     },
     compress: {
       warnings: false
     }
   }
 })

3.happypack

happypack 的原理是讓loader能夠多進程去處理文件,原理如圖示:

happypack

此外,happypack同時還利用緩存來使得rebuild 更快

var HappyPack = require('happypack'),
  os = require('os'),
  happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

modules: {
    loaders: [
      {
        test: /\.js|jsx$/,
        loader: 'HappyPack/loader?id=jsHappy',
        exclude: /node_modules/
      }
    ]
}

plugins: [
    new HappyPack({
      id: 'jsHappy',
      cache: true,
      threadPool: happyThreadPool,
      loaders: [{
        path: 'babel',
        query: {
          cacheDirectory: '.webpack_cache',
          presets: [
            'es2015',
            'react'
          ]
        }
      }]
    }),
    //若是有單獨提取css文件的話
    new HappyPack({
      id: 'lessHappy',
      loaders: ['style','css','less']
    })
  ]

4.緩存與增量構建

因爲項目中主要使用的是react.js和es6,結合webpack的babel-loader加載器進行編譯,每次從新構建都須要從新編譯一次,咱們能夠針對這個進行增量構建,而不須要每次都全量構建。

babel-loader能夠緩存處理過的模塊,對於沒有修改過的文件不會再從新編譯,cacheDirectory有着2倍以上的速度提高,這對於rebuild 有着很是大的性能提高。

var node_modules = path.resolve(__dirname, 'node_modules');
var pathToReact = path.resolve(node_modules, 'react/react');
var pathToReactDOM = path.resolve(node_modules,'react-dom/index');

{
        test: /\.js|jsx$/,
        include: path.join(__dirname, 'src'),
        exclude: /node_modules/,
        loaders: ['react-hot','babel-loader?cacheDirectory'],
        noParse: [pathToReact,pathToReactDOM]
}

babel-loader讓除了node_modules目錄下的js文件都支持es6語法,注意 exclude: /node_modules/很重要,不然 babel 可能會把node_modules中全部模塊都用 babel 編譯一遍!
固然,你還須要一個像這樣的.babelrc文件,配置以下:

{
  "presets": ["es2015", "stage-0", "react"],
  "plugins": ["transform-runtime"]
}

這是一勞永逸的作法,何樂而不爲呢?除此以外,咱們還可使用webpack自帶的cache,以緩存生成的模塊和chunks以提升多個增量構建的性能。

在webpack的整個構建過程當中,有多個地方提供了緩存的機會,若是咱們打開了這些緩存,會大大加速咱們的構建

而針對增量構建 ,咱們通常使用:

webpack-dev-server或webpack-dev-middleware,這裏咱們使用webpack-dev-middleware

webpackDevMiddleware(compiler, {
                    publicPath: webpackConfig.output.publicPath,
                    stats: {
                      chunks: false,
                      colors: true
                    },
                    debug: true,
                    hot: true,
                    lazy: false,
                    historyApiFallback: true,
                    poll: true
                })

經過設置chunks:false,能夠將控制檯輸出的代碼塊信息關閉

5.減小構建搜索或編譯路徑

爲了加快webpack打包時對資源的搜索速度,有不少的作法:

  • Resolove.root VS Resolove.moduledirectories

大多數路徑應該使用 resolve.root,只對嵌套的路徑使用 Resolove.moduledirectories,這能夠得到顯著的性能提高

緣由是Resolove.moduledirectories是取相對路徑,因此比起 resolve.root會多parse不少路徑:

resolve: {
    root: path.resolve(__dirname,'src'),
    modulesDirectories: ['node_modules']
  },
  • DLL & DllReference

針對第三方NPM包,這些包咱們並不會修改它,但仍然每次都要在build的過程消耗構建性能,咱們能夠經過DllPlugin來前置這些包的構建,具體實例:https://github.com/webpack/we...

  • alias和noPase

resolve.alias 是webpack 的一個配置項,它的做用是把用戶的一個請求重定向到另外一個路徑。 好比:

resolve: {  // 顯示指出依賴查找路徑
    alias: {
        comps: 'src/pages/components'
    }
}

這樣咱們在要打包的腳本中的使用 require('comps/Loading.jsx');其實就等價於require('src/pages/components/Loading.jsx')

webpack 默認會去尋找全部 resolve.root 下的模塊,可是有些目錄咱們是能夠明確告知 webpack 不要管這裏,從而減輕 webpack 的工做量。這時會用到module.noParse 參數

在項目中合理使用 alias 和 noParse 能夠有效提高效率,雖然不是很明顯

以上配置均由本人給出,僅供參考(有些插件的官方文檔給的不是那麼明晰)

6.其餘

  • 開啓devtool: "#inline-source-map"會增長編譯時間
  • css-loader 0.15.0+ 使webpack加載變得緩慢
//css-loader 0.16.0
Hash: 8d3652a9b4988c8ad221
Version: webpack 1.11.0
Time: 51612ms

//如下是css-loader 0.14.5
Hash: bd471e6f4aa10b195feb
Version: webpack 1.11.0
Time: 6121ms
  • 對於ant-design模塊,使用babel-plugin-import插件來按需加載模塊
  • DedupePlugin插件能夠在打包的時候刪除重複或者類似的文件,實際測試中應該是文件級別的重複的文件

結尾

雖然上面的作法減小了文件體積,加快了編譯速度,總體構建(initial build)從最初的三分多鐘到一分鐘,rebuild十多秒,優化效果明顯。但對於Webpack + React項目來講,性能優化方面遠不止於此,還有不少的優化空間,好比服務端渲染,首屏優化,異步加載模塊,按需加載,代碼分割等等

相關文章
相關標籤/搜索