Webpack构建优化实践

1,885 阅读3分钟

前言

我觉得webpack的配置对于很多前端来说就是一个黑洞,首先是配置极多,其次项目大起来之后优化起来总是无从下手,后来自己通过看了众多的打包性能优化的配置之后总结下来其实就这么几点预编译,做缓存,多线程,少检索

在聊到性能优化前,我们先得知道怎么去分析我们打包性能。

性能分析工具

性能分析主要分为打包大小分析打包时间分析

打包大小分析

打包的大小分析的插件是webpack-bundle-analyzer,这个插件可以将我们打包的各模块的大小可视化的展示出来了。

首先我们在package.json中写一个命令:

// package.json
"scripts": {
    "analyzer": "npm run build && webpack-bundle-analyzer --port 8888 ./dist/analyzer.json"
}

webpack.prod.js代码:

// webpack.prod.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
...

plugins:{
    ...
    new BundleAnalyzerPlugin({
      analyzerMode: 'disabled',
      generateStatsFile: process.env.Analyzer === 'on',
      statsFilename: path.join(__dirname, '../', 'dist/analyzer.json')
    })
}

效果如图所示:

image.png 当然我们得到了我们项目模块的大小详情之后,我们可以优化打出来比较大的模块,比如lodash,我们并不需要把所有的方法都打进来的,可以通过只提取使用到的方法即可。

打包时间分析

当然webpack打包我们除了要知道所有的打包出来的模块大小,还需要分析每个打包环节所花的时间,从而优化不同环节的耗时操作,对此我们要下载speed-measure-webpack-plugin插件。

其实配置很简单,只需要在我们的webpack配置包一层即可。

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

const smp = new SpeedMeasurePlugin();

module.exports = smp.wrap(webpackConfig) // webpackConfig指打包配置

各个环节所消耗的时间如下:

image.png

多线程

HappyPack

由于构建需要对大量文件进行解析和处理,所以构建是文件读写和计算密集型操作。当文件增多的时候,webpack构建速度会越来越慢,因为webpack是运行在Node.js的单线程模型,所以webpack在构建时只能一个一个处理任务,无法一次性处理多个任务。

让webpack支持多线程的话有两个方式:HappyPack(不维护)和thread-loader

HappyPack配置

const HappyPack = require('happypack');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module.exports = {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'happypack/loader?id=jsx'
      },
    ],
    plugins: [
      new HappyPack({
        id: 'jsx',
        threadPool: happyThreadPool,
        loaders: [
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: '.webpack_cache'
            }
          }
        ]
      })
    ]
}

这边我们使用babel-loader时使用指定的目录将用来缓存 loader 的执行结果。之后的 webpack 构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的 Babel 重新编译过程。

当然并不是所有的loader都需要用happypack开启多线程,因为多线程本身的启动也是需要时间的,项目不大或者不是耗时的loader可选择不开启。

thread-loader

thread-loader配置: thread-loader配置相比于happypack会更加简单。

module.exports = {
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                use: ['thread-loader', 'babel-loader']
            }
        ]
    }
}

缓存

cache-loader

因为每次webpack打包编译会把所有的文件重新打包编译一遍,这也意味着很多文件没有修改也会重新编译,实际上这样也会导致构建时间的增多,在性能开销较大的loader,可以用cache-loader将结果缓存下来。

module.exports = {
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                use: ['cache-loader', 'babel-loader']
            }
        ]
    }
}

上文我们看到其实babel-loader自带了缓存功能,但是可能有些loader没有这种配置,可使用cache-loader缓存编译结果。

webpack5持久化缓存

当然在webpack4中我们可以使用cache-loader或者babel-loader自带的缓存,但是cache-loader只作用于loader的执行结果。在webpack5中内置了持久化缓存。

基础模块抽离

webpack-dll-pllugin

基础模块抽离主要就是将一些不会经常变更的第三方依赖,单独抽离出来。例如我们在项目里面常用的react全家桶,lodash等。

实际上我们每次打包都要去编译这些几乎不需要变更的第三方依赖库,这会导致我们浪费很多时间,我们可以使用webpack-dll-plugin库以一种预编译的方式,将这些基础模块提前打包成一个个动态链接库(一个链接库可包括多个模块),之后每次打包的时候就不用再去编译这些基础库,只要这些第三方依赖库的版本没有改变,我们就不需要重新去编译。

externals

除了可以使用webpack-dll-plugin去将基础库进行预编译,还可以使用CDN引入这些库,并配合webpackexternals配置不将这些库打包进去以优化构建速度。

index.html:

<script
  src="https://code.jquery.com/jquery-3.1.0.js"
  crossorigin="anonymous"
></script>

webpack.config.js:

module.exports = {
    //...
    externals: {
        'jquery': 'jQuery'
    }
}

这样通过import $ from 'jquery';依然可以使用jquery。

缩小文件搜索范围

resolve.modules

由于webpack搜索第三方依赖库,先会搜索./node_modules,然后没搜索到会继续往上一层../node_modules,以此内推。因此我们可以指定好第三方依赖库的路径,减少搜索时间。

const path = require('path');
module.exports = {
    //...
    resolve: {
        modules: [path.resolve(__dirname, 'node_modules')],
    }
}

resolve.extensions

当我们导入的文件没有后缀的时候,我们可以通过指定resolve.extensions来告诉webpack的后缀的搜索顺序,一般频率最高的放在最前面以此来减少搜索次数。

module.exports = {
    //...
    resolve: {
        extensions: ['jsx','js','json']
    }
}

module.noParse

由于一些如jquerychartjs等库没有实现模块化标准,这样解析这些库会浪费时间而且没有意义。

module.exports = {
    //...
    module: {
        noParse: /jquery/
    }
}

loader下的exclude和include

webpackloader编译模块时,我们可以指定exclude和include属性,exclude表示哪些文件夹下的模块不需要编译,include表示哪些文件夹下模块需要编译,两者同样使用的是绝对路径。

const path = require('path');
module.exports = {
    //...
    module: {
        rules: [
            {
                test: /\.js[x]?$/,
                use: ['babel-loader'],
                exclude: [path.resolve(__dirname, 'node_modules')]
            }
        ]
    },
}

参考:

深入浅出 Webpack