【webpack系列】webpack构建速度和体积优化策略

1,432 阅读4分钟

1. 初级分析:使用webpack内置的stats

stats构建的统计信息

// package.json
{
	...
    "scripts": {
    	"build:stats": "webpack --env production --json > stats.json"
        ...
    }
    ...
}

nodejs中

const webpack = require('webpack');
const prodConfig = require('../../compile/webpack.prod.config');

webpack(prodConfig, (err, stats) => {
  if (err) {
    console.log(err);
    process.exit(2);
  }
  if(stats.hasErrors()) {
  	return console.error(stats.toString('errors-only'));
  }
  console.log(stats);
})

缺点:颗粒度太粗,看不出问题所在。

2. 速度分析:使用speed-measure-webpack-plugin

分析整体打包的总耗时,以及每个loader和插件的执行耗时情况。

  1. 安装:npm install speed-measure-webpack-plugin -D
  2. 在webpack.config.js中引入
// webpack.config.js
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const sm= = new SpeedMeasurePlugin();
// 用sm.wrap() 包裹webpack配置对象
const webpackConfig = sm.wrap({
	entry: './src/index.js',
    ...
	plugins: [
    	...
    ]
});
module.exports = webpackConfig;
  1. 构建:npm run build

3. 体积分析:使用webpack-bundle-analyzer

构建完成后,会在8888端口展示大小,因此可以分析依赖的第三方模块文件的大小和业务代码中组件的大小。

  1. 安装:npm i webpack-bundle-analyzer -D
  2. 在webpack.config.js中引入
// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
	...
    plugins:[
    	...
    	new BundleAnalyzerPlugin();
    ]
    ...
}
  1. 构建:npm run build

4. 使用高版本的webpack和nodejs

加快构建速度,减少构建时间。

  1. 使用webpack4优化原因:
  • v8带来的优化(for of替代forEach,Map/Set替代Object,includes替代indexOf)
  • 默认使用更快的md4 hash算法,替代md5
  • webpack4 AST可以直接从loader传递给AST,减少解析时间
  • 使用字符串方法替代正则表达式
  1. 高版本node解析耗时更少 执行同一个js文件,对比如下:

5. 多进程多实例构建

资源解析并行可选方案

  1. HappyPack
  2. thread-loader
1. 使用HappyPack解析资源

原理:每次webpack解析一个模块,HappyPack会将它以及它的依赖分配给worker线程中

  1. 安装:npm install happypack -D
  2. 使用:
const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.js$/,
        //把对.js 的文件处理交给id为happyBabel 的HappyPack 的实例执行
        loader: 'happypack/loader?id=happyBabel',
        //排除node_modules 目录下的文件
        exclude: /node_modules/
      },
    ]
  },
plugins: [
    new HappyPack({
        //用id来标识 happypack处理那里类文件
      id: 'happyBabel',
      //如何处理  用法和loader 的配置一样
      loaders: [{
        loader: 'babel-loader?cacheDirectory=true',
      }],
      //共享进程池
      threadPool: happyThreadPool,
      //允许 HappyPack 输出日志
      verbose: true,
    })
  ]
}
HappyPack 参数
  • id: String,用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件.
  • loaders: Array,用法和 webpack Loader 配置中一样.
  • threads: Number,代表开启几个子进程去处理这一类型的文件,默认是3个,类型必须是整数。
  • verbose: Boolean,是否允许 HappyPack 输出日志,默认是 true。
  • threadPool: HappyThreadPool 代表共享进程池,即多个 HappyPack 实例都使用同一个共享进程池中的子进程去处理任务,以防止资源占用过多。 verboseWhenProfiling: Boolean,开启webpack --profile ,仍然希望HappyPack产生输出。 debug: Boolean 启用debug 用于故障排查。默认 false。

HappyPack不怎么维护了: happypack

2. 使用thread-loader解析资源

原理:每次webpack解析一个模块,会将它以及它的依赖分配给worker线程中

  1. 安装thread-loader: npm i thread-loader -D
  2. 使用:
module: {
    rules: [
        {
            test: /\.js$/,
            include: path.resolve('src'),
            use: [
                {
                    loader: 'thread-loader',
                    // 有同样配置的 loader 会共享一个 worker 池(worker pool)
                    options: {
                      // 产生的 worker 的数量,默认是 cpu 的核心数
                      workers: 2,

                      // 一个 worker 进程中并行执行工作的数量
                      // 默认为 20
                      workerParallelJobs: 50,

                      // 额外的 node.js 参数
                      workerNodeArgs: ['--max-old-space-size', '1024'],

                      // 闲置时定时删除 worker 进程
                      // 默认为 500ms
                      // 可以设置为无穷大, 这样在监视模式(--watch)下可以保持 worker 持续存在
                      poolTimeout: 2000,

                      // 池(pool)分配给 worker 的工作数量
                      // 默认为 200
                      // 降低这个数值会降低总体的效率,但是会提升工作分布更均一
                      poolParallelJobs: 50,

                      // 池(pool)的名称
                      // 可以修改名称来创建其余选项都一样的池(pool)
                      name: "my-pool"
                    }
                }, 
                'babel-loader'
            ]
        }
    ]
},

多进程/多实例并行压缩

  1. 使用parallel-uglify-plugin插件并行压缩
  2. 使用uglifyjs-webpack-plugin,开启parallel参数
  3. 使用terser-webpack-plugin,开启parallel参数

6. 预编译资源模块

使用DLLPlugin进行分包,DllReferencePligin 对 manifest 引用,提升打包速度。

使用DLLPlugin进行分包步骤
  1. 创建一个配置文件 webpack.dll.config.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    vendor: [
      'react',
      'react-dom'
    ],
  },
  output: {
    filename: '[name]_[chunkhash].dll.js',
    path: path.join(__dirname, 'build/library'),
    library: '[name]',
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]_[hash]',
      path: path.join(__dirname, 'build/library/[name].json')
    }) 
  ]
};
  1. 在package.json中添加dll命令
// package.json
{
	...
    scripts: {
    	...
        "dll": "webpack --config webpack.dll.config.js"
    }
    ...
}
  1. 运行 npm run dll,会生成一个build/library文件夹,里面包含两个文件 这个打包后的vendor.js是一个自执行函数,返回的是__webpack_require__这个函数,并且赋值给了vendor变量。而第三方模块都会存储在闭包中。当外部调用vendor(id)的时候,就会执行__webpack_require__(id),获取第三方模块。

  2. 在webpack.config.js plugins中引入build/library中的json文件

const webpack = require('webpack');

module.exports = {
	...
    plugins: [
    	new webpack.DllReferencePlugin({
            manifest: require('./build/library/vendor.json')
        })
    ],
    ...
};

使用dll进行分包前后打包速度对比:

dll流程
  1. 通过dllPlugin生成 vendor.json和vendor.js,vendor.js会自执行返回一个加载函数vendor(名字可配置),通过闭包将模块存储在内存中,注意vendor是一个全局变量。
  2. webpack通过DllReferencePlugin在打包的时候分析业务代码中使用了哪些第三方模块,哪些模块是不需要打包进业务代码中,而是去vendor.js中获取。
  3. vendor中获取的模块是通过调用全局函数vendor(id)来进行引入。

7. 利用缓存,提升二次构建速度

  1. babel-loader开启缓存
  2. terser-webpack-plugin开启缓存
  3. 使用cache-loader或者hard-source-webpack-plugin

8. 缩小构建目标

尽可能的少构建模块

  • babel-loader不解析node_modules
module.exports = {
	module: {
    	rules: [
        	{
            	test: /\.js$/,
                loader: 'babel-loader',
                exclude: 'node_modules'
            }
        ]
    }
};
  • 优化resolve.modules配置,减少模块搜索层级
  • 优化resolve.mainFields配置
  • 优化resolve.extensions配置
  • 合理使用alias
resolve: {
    extensions: ['', '.js', '.vue', '.json'],
    alias: {
        'src': path.resolve(__dirname, '../src'),
        'components': path.resolve(__dirname, '../src/components'),
        'webview': path.resolve(__dirname, '../src/webview'),
        'lib': path.resolve(__dirname, '../src/lib'),
    }
}

9. tree shaking擦除无用的css

使用purgecss-webpack-plugin,和mini-css-extract-plugin配合使用,擦除无用的css purgecss-webpack-plugin具体用法

10. 使用webpack进行图片压缩

使用image-webpack-loader

11. 使用动态polyfill服务

使用polyfill service,他会识别user agent,下发不同的polyfill。

根据polyfill.io官方提供的服务