webpack性能优化

宫元徽
2023-12-01

1. 前言

本文适用于使用 webpack 或者基于 webpack 的脚手架(如:vue-cli)开发的项目。

1.1 为什么需要考虑 webpack 的性能优化

  • 随着项目的页面越来越多,功能模块将会越来越多,相应的 webpack 的构建时间也会越来越长,对开发效率的影响也会越来越大。
  • 我们做的移动端的 web 应用,我们想优化应用打包之后的大小。

特别说明:
本文仅简单罗列一些关于 webpack 的优化项,webpack 的发展速度非常快,当前的方案可能在未来已经无法使用,请知悉。文章编写时间:2021-11。本文所列举的示例均以webpack@4为基础。

2. 如何优化?

2.1 分析

2.1.1 官方分析工具

在 webpack 构建时,追加--profile --json > stats.json,将得到stats.json文件,然后使用官方的分析网站来分析该文件。我们将得到打包时间、模块数量以及每个模块的大小、错误和警告的列表及详情、chunk 数量等信息,然后用分析工具分析(分析工具充当的只是图形化、格式化展示的角色而已)出来的信息,大致的定位问题。

2.1.2 webpack-bundle-analyzer

这是一个 webpack 的插件,将其配置到 webpack 的选项中即可。该工具能很直观的给出每一个打包出来的文件的大小以及各自的依赖和具体路径,能够更加方便的帮助我们对项目进行分析。请按需启用。

使用示例:

npm install --save-dev webpack-bundle-analyzer

// package.json

{
  "scripts": {
    "build-analyze": "cross-env NODE_ENV=production IS_ANALYZE=true npm run build"
  }
}
const isAnalyze = !!process.env.IS_ANALYZE
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')

const plugins = [
    // ...
]

if (isAnalyze) {
    new BundleAnalyzerPlugin(),
}

const config = {
  // ...
  plugins,
  // ...
}

2.1.3 speed-measure-webpack-plugin

这是一个 webpack 的插件,将其配置到 webpack 的选项中即可。它可以帮助我们分析整个打包的总耗时,以及每一个 loader 和每一个 plugins 构建所耗费的时间,从而帮助我们快速定位 webpack 配置中可以改进的问题。

使用示例:

npm install --save-dev speed-measure-webpack-plugin
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')

const smp = new SpeedMeasurePlugin()

const webpackConfig = smp.wrap({
  plugins: [
    // ...
  ],
})

2.2 进行改进

2.2.1 升级 webpack 版本

webpack 的每一个版本的更新,在其内部都做了大量的优化和重构。依惯例,升级版本一定能带来性能提升,而且提升效果很明显。比如:Tree Shaking(对 ES6 Modules 生效),在 webpack@4 的时候,就已经可以使用了。

缺点:

  • 需要重新引入 webpack 所需的依赖包。在升级 webpack 之后,往往伴随着 webpack 使用方式的变更,原有的依赖包,有可能没办法正常执行,如果遇到问题,需要逐一分析依赖包,找出其问题所在,然后找到适合于当前 webpack 的依赖包版本。

2.2.2 优化 loader

项目越大,需要转换代码越多,效率就越低。我们可以通过配置exlcude的方式来减少待转换文件的数量。

使用示例:

const config = {
  //...
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
  //...
}

2.2.3 HappyPack

HappyPack 通过并行转换文件使初始 webpack 构建更快。

npm install --save-dev happypack
const path = require('path')
const os = require('os')
const HappyPack = require('happypack')
const happyThreadPool = HappyPack.ThreadPool({
  size: Math.floor(os.cpus().length * 0.8),
})

const config = {
  // ...
  plugins: [
    // ...
    new HappyPack({
      id: 'js',
      loaders: ['babel-loader'],
      threadPool: happyThreadPool,
      verbose: true,
    }),
    // ...
  ],
  module: {
    rules: [
      // ...
      {
        test: /\.js[x]?$/,
        include: [path.resolve('src')],
        exclude: /node_modules/,
        loader: 'happypack/loader?id=js',
      },
      // ...
    ],
  },
  // ...
}

2.2.4 dll-plugin

这是 webpack 内置的优化方法。类似于 windows 的 dll 文件。

使用示例:

// webpack.dll.config.js

const webpack = require('webpack')
const os = require('os')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const {
  projectPath,
  packageJSON,
  dllConfig,
  dllPath,
  isProduction,
} = require('./consts')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const stats = require('./webpack-config/stats.js')
const dllConfigPath = path.join(projectPath, 'dll.config.js')
const dllConfig = require(dllConfigPath)
const { dllArray } = dllConfig

module.exports = {
  context: projectPath,
  entry: {
    vendor1: [
      'vue',
      'vue-router',
      'vuex',
      'vue-class-component',
      'vuex-class',
      'vue-property-decorator',
      'axios',
      'throttle-debounce',
    ],
    vendor2: [...dllArray],
  },
  output: {
    path: path.join(projectPath, dllPath),
    filename: '[name].dll.js',
    pathinfo: true,
    library: '[name]_dll',
  },
  resolve: {
    extensions: ['.ts', '.js', '.vue'],
    mainFiles: ['index'],
    modules: [path.resolve(projectPath, 'node_modules'), 'node_modules'],
  },
  stats,
  plugins: [
    new CleanWebpackPlugin({
      verbose: true,
      cleanStaleWebpackAssets: false,
      cleanOnceBeforeBuildPatterns: ['**/*'],
    }),
    new VueLoaderPlugin(),
    new webpack.DllPlugin({
      context: projectPath,
      path: path.resolve(projectPath, dllPath, 'manifest.json'),
      name: '[name]_dll',
    }),
  ],
  optimization: {
    minimizer: [
      new TerserPlugin({
        test: /\.js$/i,
        parallel: os.cpus().length,
        sourceMap: true,
        extractComments: false,
        terserOptions: {
          ecma: 5,
          warnings: false,
          parse: {},
          compress: {},
          mangle: true,
          keep_classnames: false,
          keep_fnames: false,
          output: {
            beautify: false,
            comments: false,
            // 只用单引号
            quote_style: '1',
          },
        },
      }),
    ],
  },
  module: {
    rules: [
      { test: /\.vue$/, use: ['vue-loader'] },
      {
        test: /\.s?css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'sass-loader',
          'postcss-loader',
        ],
      },
      {
        test: /\.ts$/,
        loader: 'ts-loader',
        options: { appendTsSuffixTo: [/\.vue$/] },
        exclude: /node_modules/,
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: ['babel-loader'],
      },
    ],
  },
}

// dll.config.js

module.exports = {
  dllArray: [
    'muse-ui',
    'muse-ui-message',
    'muse-ui-toast',
    'numbro',
    'vue-awesome-swiper',
    'vue-dplayer',
    'vue-json-viewer',
    'vue-star-rating',
    'vuedraggable',
    'weixin-js-sdk',
  ],
}

// package.json

{
  "scripts": {
    "build-dll": "webpack -p --progress --config build/webpack.dll.config.js"
  }
}

// public/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta
      name="viewport"
      content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no"
    />
    <title>loading...</title>
  </head>

  <body>
    <div id="app"></div>
    <script src="vendor/vendor1.dll.js"></script>
    <script src="vendor/vendor2.dll.js"></script>
    <!-- built files will be auto injected -->
  </body>
</html>

// webpack.config.js

const projectPath = process.cwd()
const webpack = require('webpack')

const config = {
  // ...
  plugins: [
    // ...
    new webpack.DllReferencePlugin({
      context: projectPath,
      manifest: require(path.join(projectPath, 'public/vendor/manifest.json')),
    }),
    // ...
  ],
  // ...
}

2.2.5 合理使用 source-map

打包生成 sourceMap 的时候,如果信息越详细,打包速度就会越慢。

  • eval: 生成代码 每个模块都被 eval 执行,并且存在@sourceURL
  • cheap-eval-source-map: 转换代码(行内) 每个模块被 eval 执行,并且 sourcemap 作为 eval 的一个 dataurl
  • cheap-module-eval-source-map: 原始代码(只有行内) 同样道理,但是更高的质量和更低的性能
  • eval-source-map: 原始代码 同样道理,但是最高的质量和最低的性能
  • cheap-source-map: 转换代码(行内) 生成的 sourcemap 没有列映射,从 loaders 生成的 sourcemap 没有被使用
  • cheap-module-source-map: 原始代码(只有行内) 与上面一样除了每行特点的从 loader 中进行映射
  • source-map: 原始代码 最好的 sourcemap 质量有完整的结果,但是会很慢

2.2.6 terser-webpack-plugin

webpack v5 开箱即带有最新版本的 terser-webpack-plugin。如果你使用的是 webpack v5 或更高版本,同时希望自定义配置,那么仍需要安装 terser-webpack-plugin。如果使用 webpack v4,则必须安装 terser-webpack-plugin v4 的版本。

使用示例(webpack@4):
// webpack.config.js

const TerserPlugin = require('terser-webpack-plugin')

const config = {
  // ...
  optimization: {
    minimizer: [
      new TerserPlugin({
        test: /\.js$/i,
        parallel: os.cpus().length,
        sourceMap: true,
        extractComments: false,
        terserOptions: {
          ecma: 5,
          warnings: false,
          parse: {},
          compress: {},
          mangle: true,
          keep_classnames: false,
          keep_fnames: false,
          output: {
            beautify: false,
            comments: false,
            // 只用单引号
            quote_style: '1',
          },
        },
      }),
    ],
  },
  // ...
}

2.2.7 IgnorePlugin

可忽略符合匹配条件的模块。

// webpack.config.js

const webpack = require('webpack')

const config = {
  // ...
  plugins: [
    // ...
    new webpack.IgnorePlugin({
      checkResource: resourcePath => {
        if (/moment\/locale\/(?!zh-cn)/.test(resourcePath)) {
          return true
        }
        return false
      },
    }),
    // ...
  ],
  // ...
}

2.2.8 optimize-css-assets-webpack-plugin

这个插件使用 cssnano 来优化和缩小你的 CSS。

// webpack.config.js

const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')

const config = {
  // ...
  optimization: {
    // ...
    minimizer: [
      new OptimizeCssAssetsPlugin({
        assetNameRegExp: /\.(sc|le|c)ss$/,
        cssProcessor: require('cssnano'),
        cssProcessorPluginOptions: {
          preset: ['default', { discardComments: { removeAll: true } }],
        },
        canPrint: true,
      }),
      // ...
    ],
    // ...
  },
  // ...
}

2.2.9 其他优化项

  • alias: 配置别名能让 Webpack 更快找到路径。
  • noParse: 防止 webpack 解析任何匹配给定正则表达式的文件。被忽略的文件不应调用 import、require、define 或任何其他导入机制。这可以在忽略大型库时提高构建性能。
  • cache-loader: 允许在磁盘(默认)或数据库中缓存后续加载器的结果。
 类似资料: