在去年年底參與的一個項目中,項目技術棧使用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
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存在的不足出發,優化不足,提高效率
1.減少打包文件體積
webpack+react的項目打包出來的文件常常動則幾百kb甚至上兆,究其緣由有:
針對第一種狀況,咱們可使用 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'] }) ]
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打包時對資源的搜索速度,有不少的作法:
大多數路徑應該使用 resolve.root
,只對嵌套的路徑使用 Resolove.moduledirectories
,這能夠得到顯著的性能提高
緣由是Resolove.moduledirectories
是取相對路徑,因此比起 resolve.root
會多parse不少路徑:
resolve: { root: path.resolve(__dirname,'src'), modulesDirectories: ['node_modules'] },
針對第三方NPM包,這些包咱們並不會修改它,但仍然每次都要在build的過程消耗構建性能,咱們能夠經過DllPlugin來前置這些包的構建,具體實例:https://github.com/webpack/we...
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.其餘
//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
babel-plugin-import
插件來按需加載模塊雖然上面的作法減小了文件體積,加快了編譯速度,總體構建(initial build)從最初的三分多鐘到一分鐘,rebuild十多秒,優化效果明顯。但對於Webpack + React項目來講,性能優化方面遠不止於此,還有不少的優化空間,好比服務端渲染,首屏優化,異步加載模塊,按需加載,代碼分割等等