SunShinewyf/webpack-demo

webpack的性能优化

SunShinewyf opened this issue · 0 comments

webpack打包慢似乎称为业界广为诟病的一个话题,所以笔者通过查阅资料和自身实践来总结一下webpack的性能优化这方面的tips

webpack性能分析工具

进入到项目根目录并执行如下命令:

webpack --profile --json > stats.json

这时就会在根目录生成一个stats.json文件,然后将该文件上传到上面列出的网站页面上,就可以对webpack打包的性能消耗情况

webpack的优化点

webpack的配置如下:

const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const OpenBrowserPlugin = require('open-browser-webpack-plugin');
const casProxy = require('./proxy');

module.exports = {
  entry: {
    js: './app/client.js',
    vendor: [
      'react', 'classnames', 'react-router', 'react-dom',
    ],
  },
  output: {
    filename: '[name].js',
    path: path.join(__dirname, 'dist'),
    chunkFilename: '[name].js'
  },
  resolve: {
    extensions: ['', '.js', '.json'],
  },
  module: {
    loaders: [
      {
        test: /\.js[x]?$/,
        loader: 'react-hot!babel',
      },
      {
        test: /\.less$/,
        loader: 'style!css!postcss!less',
      },
      {
        test: /\.css/,
        loader: 'style!css',
      },
      {
        test: /\.(png|jpg)$/,
        loader: 'url-loader?limit=8192',
      },
    ],
  },
  plugins: [
    new webpack.DefinePlugin({
      "process.env": {
         NODE_ENV: JSON.stringify("production")
       }
    }),
    new webpack.optimize.OccurenceOrderPlugin(),
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'app/index.html'),
    }),
    new OpenBrowserPlugin({
      url: 'http://localhost:3000'
    }),
  ],
  devtool: 'source-map',
  devServer: {
    contentBase: './app/',
    historyApiFallback: true,
    hot: true,
    proxy: casProxy(),
    host: '0.0.0.0'
  },
}

按上面的配置文件进行编译,首次编译的时间如下:

images

webpack visualizer中显示的性能分析如下:

images

可以看到消耗在node_modules中的占比高达94%,而且等待webpack编译的时间十分漫长,每一次的小更新都需要等待那么长的时间才可以看到刷新页面的效果,明显不太符合常理,如何优化?

babel-loader中exclude node_modules

babel-loader的功能就是将项目中使用到ES6或者jsx的代码转化为es5,并且会把项目中所有的js文件都进行转化一遍,其中包括node_modules文件里面的内容。如果一开始就把node_modules文件
排除在外,就可以省去很多时间。
将babel-loader中的配置配置成如下:

  {
        test: /\.js[x]?$/,
        exclude: /node_modules/,
        loader: 'react-hot!babel',
  },

images

看stats.json中的时间字段,减少了很多

使用CommonsChunkPlugin提取公共代码

由于像react这些公共代码会被很多页面使用,所以将这些公共代码进行提取之后,就可以节省一些时间。实践一下发现时间如下:

images

还是优化了一些了的

使用alias

通过设置resolve中的alias配置项,可以让webpack在搜索的时候直接去指定的路径进行查找,从而省略掉搜索的时间。添加alias的配置如下:

  resolve: {
    extensions: ['', '.js', '.json'],
    alias: {
      components: __dirname + '/app/components',
    },
  },

这样在项目文件中引入components时,就相当于引入/app/components

使用happypack

happypack的原理使用多进程去处理loader,在一些比较简单的项目中,这个插件的优势并没有那么明显,但是当需要babel-loader去处理大量jsx,es6文件的项目中,
可能会有一定的效果。webpack的配置文件中加入happypack时如下(只贴出加入happypack的部分):

  module: {
    loaders: [
      {
        test: /\.js[x]?$/,
        exclude: /node_modules/,
        loader: path.resolve(__dirname, './node_modules', 'happypack/loader') + '?id=js'
      },
    ],
  },
plugins: [
...
    new HappyPack({
        id: 'js',
        loaders: ['react-hot!babel'],
        cache: false,
        verbose: true
    }),
  ],

加入happypack后的打包时间如下:

images

相比于上一次,还是减少了不少。但不是所有的项目使用这个都可以进行编译优化的,除非你的项目打包过程中有很多耗费cpu的操作,否则也没有什么效果

css-loader 使用低于0.15.0的版本

有些资料上显示css-loader的版本太高会导致编译过慢,所以我也去试了一下0.14版本的,感觉没多大差别,不过在实在找不出优化点的时候,可以试一下

压缩代码

uglifyPlugin的压缩过程在开发环境中很耗时,所以有人推荐使用webpack-uglify-parallel。在webpack的配置中加入这个插件:

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

我按照一些资料上试了一下之后,并没有看到效果(捂脸)

将css文件独立出来,优化bundle的体积

   {
      test: /\.css/,
      loader: extractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader' })
    },
    ...
     new extractTextPlugin('[name].[contenthash:4].css'),

这个也实践了一下,确实有所改善,但是效果不是特别明显:

images

使用dllPlugin

这个插件的思路是借鉴了window系统的dll,表示一个单纯的依赖包。它的作用是不仅可以将公用代码提取出来放到一个独立的文件,从而使不同的文件使用。除此之外,它还可以将公用代码和业务代码在编译过程中
就分离出来。当你的业务代码发生改变时,公用代码也不会因此发生改变。当因为业务代码的改变而是公用代码的hash发生改变时,可以使用这样方式来规避这种情况。
dllPlugin&DllReferencePlugin的大概流程如下:

  • 建立一个简单的webpack.dll.config.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
    entry: {
        vendor: ['react', 'react-dom','react-router','react-dom']
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name].dll.js',
        library: '[name]_library',
        context: __dirname
    },
    plugins: [
        new webpack.DllPlugin({
            path: 'manifest.json',
            name: '[name]_library'
        })
    ]
};
  • 执行下列命令,生成manifest.json文件
webpack --progress --colors --config ./webpack-dll.config.js
  • 在原先的webpack.config.js文件中使用DllReferencePlugin
  plugins: [
        new webpack.DllReferencePlugin({
            context: __dirname,
            manifest: require('./manifest.json'),
        }),
    ],

使用之后然后进行编译,花费的时间如下所示:

images

使用 noParse

module.noParse 配置哪些文件可以脱离webpack的解析。 有些库是自成一体不依赖其他库的没有使用模块化的,比如jqueychart.js,要使用它们必须整体全部引入。
webpack是模块化打包工具完全没有必要去解析这些文件的依赖,因为它们都不依赖其它文件体积也很庞大,要忽略它们配置如下:

module: {
    noParse: /node_modules\/(jquey|moment|chart\.js)/
  }

提取第三方插件

这种方法主要是从减少bundle大小思路进行入手的,因为如果将一些库和项目代码一起打包,会导致体积很大,需要将其分离。比如对于react项目来说,将react相关的一些库单独进行打包,就可以减少项目文件的体积,
这种提取方案在webpack中的设置如下:

entry: {
    js: './app/client.js',
    vendor: [
      'react', 'classnames', 'react-router', 'react-dom',
    ],
  },

这样打包之后就会出现一个js.js和一个vendor.js,前者为业务逻辑打包后的代码,后者为第三方库打包后的代码。这样配置之后,在页面中也需要将vendor.js引入。引入方式如下所示:

 <script src="/build/js.js"></script>
 <script src="/build/vendor.js"></script>

这样配置之后,打包时间再一次减少:

images

这种方式和配置externals的效果是一样的,如果使用externals方式的话,那么webpack的配置方式如下所示:

{
  externals: {
     'react': 'React'
     'classnames':'classnames',
     'reactDom':'react-dom',
     'reactRouter':'react-router'
  }
}

这种的引用方式如下:

<script src="//cdn.com/react/react.min.js"></script>
<script src="/dist/js.js"></script>

参考资料: