webpack打包分析與性能優化

Eduardo8408 7年前發布 | 26K 次閱讀 性能優化 前端技術 webpack

webpack打包分析與性能優化

背景

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

下面我將在項目中遇到的問題和技術方案沉淀出來與大家做個分享

從項目自身出發

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

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

1.使用yarn而不是npm

由于項目使用npm安裝包,容易導致在多關聯依賴關系中,很可能某個庫在指定依賴時沒有指定版本號,進而導致不同設備上拉到的package版本不一。yarn不管安裝順序如何,相同的依賴關系將以相同的方式安裝在任何機器上。當關聯依賴中包括對某個軟件包的重復引用,在實際安裝時將盡量避免重復的創建。yarn不僅可以緩存它安裝過的包,而且安裝速度快,使用yarn無疑可以很大程度改善工作流和工作效率

2.刪除沒有使用的依賴

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

import a from 'abc';

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

webpack打包分析

1.打包過程分析

我們知道,webpack 在打包過程中會針對不同的資源類型使用不同的loader處理,然后將所有靜態資源整合到一個bundle里,以實現所有靜態資源的加載。webpack最初的主要目的是在瀏覽器端復用符合CommonJS規范的代碼模塊,而CommonJS模塊每次修改都需要重新構建(rebuild)后才能在瀏覽器端使用。

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

  • 對于單入口文件,每個入口文件把自己所依賴的資源全部打包到一起,即使一個資源循環加載的話,也只會打包一份

  • 對于多入口文件的情況,分別獨立執行單個入口的情況,每個入口文件各不相干

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

實例如下:

main.js引用了chunk1、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;

/*/ } /**/ ]);</code></pre>

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同時還利用緩存來使得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'] }) ]</code></pre>

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] }</code></pre>

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']
  },</code></pre> 
  
  • DLL & DllReference

針對第三方NPM包,這些包我們并不會修改它,但仍然每次都要在build的過程消耗構建性能,我們可以通過DllPlugin來前置這些包的構建

  • 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</code></pre>

  • 對于ant-design模塊,使用 babel-plugin-import 插件來按需加載模塊

  • DedupePlugin插件可以在打包的時候刪除重復或者相似的文件,實際測試中應該是文件級別的重復的文件

結尾

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

 

來自:https://segmentfault.com/a/1190000008377195

 

 本文由用戶 Eduardo8408 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!