Webpack5升级踩坑实录 打包性能优化的那些事儿

Tr° 竞一 优化 阅读 2,223
赞 12 收藏
二维码
手机扫码查看
反馈

这次打包慢到让人怀疑人生

上个月重构老项目,Webpack升级到5,结果构建速度慢得离谱。本地开发热更新要等十几秒,生产环境打包更是要两分钟起步。同事都快疯了,每天光等打包就得浪费好几个小时。

Webpack5升级踩坑实录 打包性能优化的那些事儿

折腾了半天,发现主要问题是依赖包太多,还有各种loader处理大量文件。后来试了下几种优化方案,现在开发热更新基本1-2秒,生产打包也能控制在30秒内。

缓存配置搞起来

先说缓存,这个是最立竿见影的。Webpack5自带持久化缓存,配置起来也不复杂:

module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename],
    },
  },
  // 其他配置...
}

这里我踩了个坑,一开始忘了加buildDependencies,每次修改webpack.config.js后缓存就没用了。加了之后配置文件变了才会重新生成缓存,不然就一直用旧的。

还有就是给模块加name,这样路径变化不影响缓存:

module.exports = {
  optimization: {
    moduleIds: 'deterministic',
    chunkIds: 'deterministic',
  }
}

这两个设置能确保模块ID稳定,避免因为路径变化导致缓存失效。

externals把大包扔出去

React、Vue这些框架用CDN引入,不要打进去。我们项目里React全家桶加上lodash这些工具库,打包出来几十MB,简直是噩梦:

module.exports = {
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM',
    'lodash': '_'
  }
}

对应的HTML里加上CDN链接:

<script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>

这样处理后包体积一下小了一半多。不过要注意版本兼容性,线上环境CDN可能会抽风,最好自己搭个私有CDN。

babel-loader缓存别忘了

babel编译很耗时,开启缓存能提升不少:

module.exports = {
  module: {
    rules: [
      {
        test: /.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true,
            cacheCompression: false,
          }
        }
      }
    ]
  }
}

cacheCompression设为false可以减少压缩时间,反正本地开发磁盘够用。这里踩过坑,之前设了true反而更慢。

splitChunks按需切割

代码分割这块也得仔细调,不能用默认配置:

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\/]node_modules[\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10
        },
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          priority: 5,
          enforce: true
        }
      }
    }
  }
}

vendor专门提取第三方包,common提取公共代码。enforce强制分割,不然可能分不了。priority数字越大优先级越高,这样保证第三方包先被提取。

IgnorePlugin忽略不需要的包

moment.js这种包自带各种语言包,但我们只用中文,用IgnorePlugin排除掉:

const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.IgnorePlugin({
      resourceRegExp: /^./locale$/,
      contextRegExp: /moment$/
    })
  ]
}

这样处理后包体积能小几MB,虽然现在都推荐用dayjs替代moment,但老项目改不动只能这么凑合。

terser并行压缩

生产环境压缩代码时开启多进程:

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: true,
          }
        }
      })
    ]
  }
}

parallel设为true会自动检测CPU核心数,充分利用多核优势。drop_console去掉console.log,在生产环境很有必要。

devtool模式要选对

开发环境的source map设置也有讲究,速度和调试体验需要平衡:

// 开发环境
module.exports = {
  devtool: 'eval-cheap-module-source-map'
}

// 生产环境
module.exports = {
  devtool: false  // 或者 'source-map'
}

开发用eval-cheap-module-source-map构建快,出错能定位到源码。生产环境要么关掉要么用source-map单独生成文件。

alias加快模块查找

长路径引用改成短别名,也能提升一点速度:

module.exports = {
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
      '@utils': path.resolve(__dirname, 'src/utils')
    },
    extensions: ['.js', '.jsx', '.ts', '.tsx']
  }
}

这样import ‘@components/Header’比相对路径清晰多了,查找也更快。

DllPlugin预编译不变模块

对于那些基本不变的第三方包,可以用DllPlugin提前编译好:

// webpack.dll.config.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    vendor: ['react', 'react-dom', 'lodash']
  },
  output: {
    path: path.join(__dirname, 'dll'),
    filename: '[name].dll.js',
    library: '[name]_[hash]'
  },
  plugins: [
    new webpack.DllPlugin({
      path: path.join(__dirname, 'dll', '[name]-manifest.json'),
      name: '[name]_[hash]'
    })
  ]
}

然后在主配置里引用:

plugins: [
  new webpack.DllReferencePlugin({
    manifest: require('./dll/vendor-manifest.json')
  })
]

不过现在Webpack5缓存已经很好用了,DllPlugin维护成本高,一般情况下不太需要。

最后的整合配置

把所有优化点放在一起就是:

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

module.exports = {
  mode: 'production',
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename],
    },
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    },
    extensions: ['.js', '.jsx']
  },
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM'
  },
  optimization: {
    moduleIds: 'deterministic',
    chunkIds: 'deterministic',
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\/]node_modules[\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10
        }
      }
    },
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true
      })
    ]
  },
  module: {
    rules: [
      {
        test: /.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true
          }
        }
      }
    ]
  },
  plugins: [
    new webpack.IgnorePlugin({
      resourceRegExp: /^./locale$/,
      contextRegExp: /moment$/
    })
  ]
}

这样配置下来,打包速度确实提升明显。不过还是要说,不同项目的优化策略会有差异,这套配置在我们项目里效果不错,仅供参考。

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论