Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack打包分析与性能优化 #3

Open
hawx1993 opened this issue Feb 13, 2017 · 28 comments
Open

webpack打包分析与性能优化 #3

hawx1993 opened this issue Feb 13, 2017 · 28 comments
Labels

Comments

@hawx1993
Copy link
Owner

hawx1993 commented Feb 13, 2017

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;

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

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/webpack/tree/master/examples/dll

我们使用dllplugin把第三方的NPM包生成一个名为 manifest.json 的文件,这个文件是用来让 DLLReferencePlugin 映射到相关的依赖上去的。在文件中引入该dll文件即可。其原理是通过引用 dll 的 manifest 文件来把依赖的名称映射到模块的 id 上,之后再在需要的时候通过内置的 __webpack_require__ 函数来 require 他们

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

@Thinking80s
Copy link

mark

3 similar comments
@kuigoo
Copy link

kuigoo commented Mar 28, 2017

mark

@cexoso
Copy link

cexoso commented Apr 3, 2017

mark

@yangdonglai
Copy link

mark

@Mrdapeng
Copy link

mark

@yangfan0095
Copy link

写得好!

@yangfan0095
Copy link

刚刚使用了你的方法速度提升很快 。 请问一下 在使用happypack 的时候增加缓存配置 ,打包出来缓存文件会存放在 .webpack_cache/ 下面, 但是第二次打包的时候 ,没有反应 ,要把.webpack_cache / 删除才能打包

@ghost
Copy link

ghost commented Jun 8, 2017

mark下

@cattyhuang
Copy link

mark

@ryanzhouff
Copy link

DedupePlugin插件已经被移除了。有可替代的插件的吗?现在就遇到这个问题,每次打包,就生成了一个文件(css/js)。如图所示: webpack打包文件重复

@scrapooo
Copy link

mark

1 similar comment
@xflife
Copy link

xflife commented Sep 22, 2017

mark

@wangmeijian
Copy link

m

@imwtr
Copy link

imwtr commented Nov 10, 2017

webpack-parallel-uglify-plugin插件骗了,它根本就没压缩 !-_-

@fuchao2012
Copy link

@imwtr 需要在 uglifyJS/uglifyES 中传入配置项才能生效哦,另外cache的配置我没搞清楚,ng项目里开启AOT慢的要死,求解

@imwtr
Copy link

imwtr commented Nov 13, 2017

我是直接用这个配置的,发现它并没有压缩,把warning: false 去掉之后发现没有任何提示信息
另外 那个AOT我就不清楚啦

new ParallelUglifyPlugin({
   cacheDir: '.cache/',
   uglifyJS:{
     output: {
       comments: false
     },
     compress: {
       warnings: false
     }
   }
 })

@han88829
Copy link

mark

@jweboy
Copy link

jweboy commented Apr 13, 2018

webpack-dev-server 与 webpack-dev-middleware 在性能上有取舍吗?

@ql434
Copy link

ql434 commented May 7, 2018

mark

@MapleShaw
Copy link

想问下如果已经升级到了webpack4,splitChunks是不是可以直接替代DLL & DllReference了?

@chenxiao01
Copy link

mark

1 similar comment
@simple7
Copy link

simple7 commented May 29, 2018

mark

@wangmeijian
Copy link

@imwtr 确实没压缩

@Yichang812
Copy link

mark

1 similar comment
@fishear
Copy link

fishear commented Jun 28, 2018

mark

@Rainsho
Copy link

Rainsho commented Jul 31, 2018

mark

1 similar comment
@keminu
Copy link

keminu commented Mar 20, 2019

mark

@chenchenpp
Copy link

eyes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests