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项目来讲,性能优化方面远不止于此,还有不少的优化空间,好比服务端渲染,首屏优化,异步加载模块,按需加载,代码分割等等

相关文章
相关标签/搜索