1、实现目标

本篇将从优化开发体验、加快编译速度、减小打包体积、加快加载速度 4 个角度出发,介绍如何对 webpack 项目进行优化;

2、效率工具

2.1 编译进度条

https://www.npmjs.com/package/progress-bar-webpack-plugin

安装:

  1. npm i -D progress-bar-webpack-plugin

webpack.common.js 配置方式如下:

  1. const chalk = require('chalk')
  2. const ProgressBarPlugin = require('progress-bar-webpack-plugin')
  3. module.exports = {
  4. plugins: [
  5. // 进度条
  6. new ProgressBarPlugin({
  7. format: ` :msg [:bar] ${chalk.green.bold(':percent')} (:elapsed s)`
  8. })
  9. ],
  10. }

image.png

2.2 编译速度分析

https://www.npmjs.com/package/speed-measure-webpack-plugin

安装:

  1. npm i -D speed-measure-webpack-plugin

webpack.dev.js 配置方式如下:

  1. const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
  2. const smp = new SpeedMeasurePlugin();
  3. module.exports = smp.wrap({
  4. // ...webpack config...
  5. })

效果:
image.png

2.3 打包体积分析

https://www.npmjs.com/package/webpack-bundle-analyzer

安装:

  1. npm i -D webpack-bundle-analyzer

webpack.prod.js 配置方式如下:

  1. const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
  2. module.exports = {
  3. plugins: [
  4. // 打包体积分析
  5. new BundleAnalyzerPlugin()
  6. ],
  7. }

包含各个 bundle 的体积分析,效果如下:
image.png

3、优化开发

3.1 热更新

热更新指的是,在开发过程中,修改代码后,仅更新修改部分的内容,无需刷新整个页面;

webpack.dev.js 配置方式如下:

  1. module.export = {
  2. devServer: {
  3. contentBase: './dist',
  4. hot: true, // 热更新
  5. },
  6. }

3.1 热更新React组件

使用 react-refresh-webpack-plugin热更新 react 组件;

安装:

  1. npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh

webpack.dev.js 配置方式如下:

  1. const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
  2. module.exports = {
  3. plugins: [
  4. new webpack.HotModuleReplacementPlugin(),
  5. new ReactRefreshWebpackPlugin(),
  6. ]
  7. }

4、构建速度优化

4.1 cache

webpack5 较于 webpack4,新增了持久化缓存、改进缓存算法等优化,webpack5 新特性可查看 参考资料

通过配置 webpack 持久化缓存,cache: filesystem,来缓存生成的 webpack 模块和 chunk,改善构建速度,可提速 90% 左右;

webpack.common.js 配置方式如下:

  1. module.exports = {
  2. cache: {
  3. type: 'filesystem', // 使用文件缓存
  4. },
  5. }

引入缓存后,首次构建时间将增加 15%,二次构建时间将减少 90%,效果如下:

使用前: 使用后第一次构建略微慢点: 使用后第二次构建直接起飞:
image.png image.png image.png

4.2 减少 loader、plugins

每个的 loader、plugin 都有其启动时间,尽量少地使用工具,将非必须的 loader、plugins 删除;

4.3 指定 include

为 loader 指定 include,减少 loader 应用范围,仅应用于最少数量的必要模块。

  1. module.exports = {
  2. rules: [
  3. {
  4. test: /\.(png|svg|jpg|jpeg|gif)$/i,
  5. include: [
  6. paths.resolveApp('src'),
  7. ],
  8. type: 'asset/resource',
  9. }
  10. ]
  11. }

4.4 管理资源

使用 webpack 资源模块(asset module) 代替旧的 assets loader(如 file-loader/url-loader/raw-loader 等),减少 loader 配置数量。

  1. module.exports = {
  2. rules: [
  3. {
  4. test: /\.(png|svg|jpg|jpeg|gif)$/i,
  5. include: [
  6. paths.resolveApp('src'),
  7. ],
  8. type: 'asset/resource',
  9. }
  10. ]
  11. }

4.5 优化 resolve 配置

resolve用来配置 webpack 如何解析模块,可通过优化 resolve 配置来覆盖默认配置项,减少解析范围;

4.5.1 alias

alias 可以创建 import 或 require 的别名,用来简化模块引入;

webpack.common.js 配置方式如下:

  1. module.exports = {
  2. resolve: {
  3. alias: {
  4. '@': paths.appSrc, // @ 代表 src 路径
  5. },
  6. }
  7. }

4.5.2 extensions

extensions 表示需要解析的文件类型列表。

根据项目中的文件类型,定义 extensions,以覆盖 webpack 默认的 extensions,加快解析速度;

由于 webpack 的解析顺序是从左到右,因此要将使用频率高的文件类型放在左侧,如下我将 tsx 放在最左侧;

webpack.common.js 配置方式如下:

  1. module.exports = {
  2. resolve: {
  3. extensions: ['.tsx', '.ts', '.js'],
  4. }
  5. }

4.5.3 modules

modules 表示 webpack 解析模块时需要解析的目录;

指定目录可缩小 webpack 解析范围,加快构建速度;

webpack.common.js 配置方式如下:

  1. module.exports = {
  2. resolve{
  3. modules: [
  4. 'node_modules',
  5. paths.appSrc,
  6. ]
  7. }
  8. }

4.5.4 symlinks

如果项目不使用 symlinks(例如 npm link 或者 yarn link),可以设置 resolve.symlinks: false,减少解析工作量。

webpack.common.js 配置方式如下:

  1. module.exports = {
  2. resolve: {
  3. symlinks: false,
  4. },
  5. }

4.6 多线程(thread-loader)

通过 thread-loader将耗时的 loader 放在一个独立的 worker 池中运行,加快 loader 构建速度;

安装:

  1. npm i -D thread-loader

配置:

  1. {
  2. loader: 'thread-loader',
  3. options: {
  4. workerParallelJobs: 2
  5. }
  6. },

4.7 区分环境

切忌在开发环境使用生产环境才会用到的工具,如在开发环境下,应该排除 [fullhash]/[chunkhash]/[contenthash] 等工具。

在生产环境,应该避免使用开发环境才会用到的工具,如 webpack-dev-server 等插件;

4.8 devtool

不同的 devtool 设置,会导致性能差异。在多数情况下,最佳选择是 eval-cheap-module-source-map;

webpack.dev.js配置如下:

  1. export.module = {
  2. devtool: 'eval-cheap-module-source-map',
  3. }

4.9 输出结果不携带路径信息

默认 webpack 会在输出的 bundle 中生成路径信息,将路径信息删除可小幅提升构建速度。

  1. module.exports = {
  2. output: {
  3. pathinfo: false,
  4. },
  5. };
  6. }

4.10 IgnorePlugin

IgnorePlugin 在构建模块时直接剔除那些需要被排除的模块,常用于moment和国际化;

  1. new webpack.IgnorePlugin(/\.\/locale/, /moment/)

4.11 DllPlugin

核心思想是将项目依赖的框架等模块单独构建打包,与普通构建流程区分开。

  1. output: {
  2. filename: '[name].dll.js',
  3. // 输出的文件都放到 dist 目录下
  4. path: distPath,
  5. library: '_dll_[name]',
  6. },
  7. plugins: [
  8. // 接入 DllPlugin
  9. new DllPlugin({
  10. // 动态链接库的全局变量名称,需要和 output.library 中保持一致
  11. // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
  12. // 例如 react.manifest.json 中就有 "name": "_dll_react"
  13. name: '_dll_[name]',
  14. // 描述动态链接库的 manifest.json 文件输出时的文件名称
  15. path: path.join(distPath, '[name].manifest.json'),
  16. }),
  17. ],

4.12 Externals

Webpack 配置中的 externals 和 DllPlugin 解决的是同一类问题:将依赖的框架等模块从构建过程中移除。

它们的区别在于:

  • 在 Webpack 的配置方面,externals 更简单,而 DllPlugin 需要独立的配置文件。
  • DllPlugin 包含了依赖包的独立构建流程,而 externals 配置中不包含依赖框架的生成方式,通常使用已传入 CDN 的依赖包。
  • externals 配置的依赖包需要单独指定依赖模块的加载方式:全局对象、CommonJS、AMD 等。
  • 在引用依赖包的子模块时,DllPlugin 无须更改,而 externals 则会将子模块打入项目包中。
  1. // 引入cdn
  2. <script
  3. src="https://code.jquery.com/jquery-3.1.0.js"
  4. integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  5. crossorigin="anonymous"
  6. ></script>
  7. // webpack配置
  8. module.exports = {
  9. //...
  10. externals: {
  11. jquery: 'jQuery',
  12. },
  13. };
  14. // 页面
  15. import $ from 'jquery';
  16. $('.my-element').animate(/* ... */);

5、减小打包体积

5.1 代码压缩

5.1.1 JS压缩

使用 TerserWebpackPlugin来压缩 JavaScript;

webpack5 自带最新的 terser-webpack-plugin,无需手动安装;

terser-webpack-plugin 默认开启了 parallel: true 配置,并发运行的默认数量: os.cpus().length - 1 ,本文配置的 parallel 数量为 4,使用多进程并发运行压缩以提高构建速度;

webpack.prod.js 配置方式如下:

  1. const TerserPlugin = require('terser-webpack-plugin');
  2. module.exports = {
  3. optimization: {
  4. minimizer: [
  5. new TerserPlugin({
  6. parallel: 4,
  7. terserOptions: {
  8. parse: {
  9. ecma: 8,
  10. },
  11. compress: {
  12. ecma: 5,
  13. warnings: false,
  14. comparisons: false,
  15. inline: 2,
  16. },
  17. mangle: {
  18. safari10: true,
  19. },
  20. output: {
  21. ecma: 5,
  22. comments: false,
  23. ascii_only: true,
  24. },
  25. },
  26. }),
  27. ]
  28. }
  29. }

5.1.2 CSS压缩

使用 CssMinimizerWebpackPlugin压缩 CSS 文件;

安装:

  1. npm install -D css-minimizer-webpack-plugin

webpack.prod.js 配置方式如下:

  1. const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
  2. module.exports = {
  3. optimization: {
  4. minimizer: [
  5. new CssMinimizerPlugin({
  6. parallel: 4,
  7. }),
  8. ],
  9. }
  10. }

5.2 代码分离

代码分离能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,可以缩短页面加载时间;

5.2.1 抽离重复代码

SplitChunksPlugin插件开箱即用,可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk;

webpack 将根据以下条件自动拆分 chunks:

  • 新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹;
  • 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积);
  • 当按需加载 chunks 时,并行请求的最大数量小于或等于 30;
  • 当加载初始化页面时,并发请求的最大数量小于或等于 30; 通过 splitChunks 把 react 等公共库抽离出来,不重复引入占用体积;

注意:切记不要为 cacheGroups 定义固定的 name,因为 cacheGroups.name 指定字符串或始终返回相同字符串的函数时,会将所有常见模块和 vendor 合并为一个 chunk。这会导致更大的初始下载量并减慢页面加载速度;

webpack.prod.js 配置方式如下:

  1. module.exports = {
  2. optimization: {
  3. splitChunks: {
  4. // include all types of chunks
  5. chunks: 'all',
  6. // 重复打包问题
  7. cacheGroups:{
  8. // node_modules里的代码
  9. // 第三方模块
  10. vendors:{
  11. test: /[\\/]node_modules[\\/]/,
  12. chunks: "all",
  13. // name: 'vendors', 一定不要定义固定的name
  14. priority: 10, // 优先级
  15. enforce: true
  16. },
  17. // 公共的模块
  18. common: {
  19. name: 'common', // chunk 名称
  20. priority: 0, // 优先级
  21. minSize: 0, // 公共模块的大小限制
  22. minChunks: 2 // 公共模块最少复用过几次
  23. }
  24. }
  25. }
  26. }
  27. }

5.2.2 CSS文件分离

MiniCssExtractPlugin插件将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载;

安装:

  1. npm install -D mini-css-extract-plugin

webpack.common.js 配置方式如下:

注意:MiniCssExtractPlugin.loader 要放在 style-loader 后面;

  1. const MiniCssExtractPlugin = require("mini-css-extract-plugin");
  2. module.exports = {
  3. plugins: [new MiniCssExtractPlugin()],
  4. module: {
  5. rules: [
  6. {
  7. test: /\.module\.(scss|sass)$/,
  8. include: paths.appSrc,
  9. use: [
  10. 'style-loader',
  11. isEnvProduction && MiniCssExtractPlugin.loader, // 仅生产环境
  12. {
  13. loader: 'css-loader',
  14. options: {
  15. modules: true,
  16. importLoaders: 2,
  17. },
  18. },
  19. {
  20. loader: 'postcss-loader',
  21. options: {
  22. postcssOptions: {
  23. plugins: [
  24. [
  25. 'postcss-preset-env',
  26. ],
  27. ],
  28. },
  29. },
  30. },
  31. {
  32. loader: 'thread-loader',
  33. options: {
  34. workerParallelJobs: 2
  35. }
  36. },
  37. 'sass-loader',
  38. ].filter(Boolean),
  39. },
  40. ]
  41. },
  42. };

效果:
image.png

5.2.3 最小化 entry chunk

通过配置 optimization.runtimeChunk = true,为运行时代码创建一个额外的 chunk,减少 entry chunk 体积,提高性能;

webpack.prod.js 配置方式如下:

  1. module.exports = {
  2. optimization: {
  3. runtimeChunk: true,
  4. },
  5. };
  6. }

效果:
image.png

5.3 Tree Shaking(摇树)

1个模块可能有多个⽅法,只要其中的某个方法使⽤到了,则整个⽂件都会被打到 bundle 里面去,tree shaking 就是只把⽤到的方法打入 bundle ,没⽤到的方法会在uglify阶段被擦除掉;

5.3.1 JS

JS Tree Shaking将 JavaScript 上下文中的未引用代码(Dead Code)移除,通过 package.json 的 “sideEffects” 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 “pure(纯正 ES2015 模块)”,由此可以安全地删除文件中未使用的部分;

Dead Code 一般具有以下几个特征:

  • 代码不会被执行,不可到达;
  • 代码执行的结果不会被用到;
  • 代码只会影响死变量(只写不读);

    1.webpack5 sideEffects

通过 package.json 的 “sideEffects” 属性,来实现这种方式;

  1. {
  2. "name": "your-project",
  3. "sideEffects": false
  4. }

需注意的是,当代码有副作用时,需要将 sideEffects 改为提供一个数组,添加有副作用代码的文件路径:

  1. {
  2. "name": "your-project",
  3. "sideEffects": ["./src/some-side-effectful-file.js"]
  4. }

Tree Shaking前:
image.png

Tree Shaking后:
image.png

2、对组件库引用的优化

webpack5 sideEffects 只能清除无副作用的引用,而有副作用的引用则只能通过优化引用方式来进行 Tree Shaking;

loadsh

类似 import { throttle } from ‘lodash’ 就属于有副作用的引用,会将整个 lodash 文件进行打包;

优化方式是使用 import { throttle } from ‘lodash-es’ 代替 import { throttle } from ‘lodash’, lodash-esLodash库导出为 ES模块,支持基于 ES modules 的 tree shaking,实现按需引入;

ant-design

ant-design默认支持基于 ES modules 的 tree shaking,对于 js 部分,直接引入 import { Button } from ‘antd’ 就会有按需加载的效果;

假如项目中仅引入少部分组件,import { Button } from ‘antd’ 也属于有副作用,webpack不能把其他组件进行tree-shaking。这时可以缩小引用范围,将引入方式修改为 import { Button } from ‘antd/lib/button’ 来进一步优化。

5.3.2 CSS

使用 purgecss-webpack-plugin对 CSS Tree Shaking。

安装:

  1. npm i purgecss-webpack-plugin -D

因为打包时 CSS 默认放在 JS 文件内,因此要结合 webpack 分离 CSS 文件插件 mini-css-extract-plugin 一起使用,先将 CSS 文件分离,再进行 CSS Tree Shaking;

webpack.prod.js 配置方式如下:

  1. const glob = require('glob')
  2. const MiniCssExtractPlugin = require('mini-css-extract-plugin')
  3. const PurgeCSSPlugin = require('purgecss-webpack-plugin')
  4. const paths = require('paths')
  5. module.exports = {
  6. plugins: [
  7. // 打包体积分析
  8. new BundleAnalyzerPlugin(),
  9. // 提取 CSS
  10. new MiniCssExtractPlugin({
  11. filename: "[name].css",
  12. }),
  13. // CSS Tree Shaking
  14. new PurgeCSSPlugin({
  15. paths: glob.sync(`${paths.appSrc}/**/*`, { nodir: true }),
  16. }),
  17. ]
  18. }

5.4 CDN

通过 CDN 来减小打包体积;

将大的静态资源上传至 CDN:

  • 字体:压缩并上传至 CDN;
  • 图片:压缩并上传至 CDN。

6、加快加载速度

6.1 按需加载

通过 webpack 提供的 import() 语法动态导入功能进行代码分离,通过按需加载,大大提升网页加载速度;

  1. export default function App () {
  2. return (
  3. <div>
  4. hello react 111
  5. <Hello />
  6. <button onClick={() => import('lodash')}>加载lodash</button>
  7. </div>
  8. )
  9. }


效果如下:
image.png

6.2 浏览器缓存

浏览器缓存,就是进入某个网站后,加载的静态资源被浏览器缓存,再次进入该网站后,将直接拉取缓存资源,加快加载速度。

webpack 支持根据资源内容,创建 hash id,当资源内容发生变化时,将会创建新的 hash id。

配置 JS bundle hash,webpack.common.js 配置方式如下:

  1. module.exports = {
  2. // 输出
  3. output: {
  4. // 仅在生产环境添加 hash
  5. filename: ctx.isEnvProduction ? '[name].[contenthash].bundle.js' : '[name].bundle.js',
  6. },
  7. }

配置 CSS bundle hash,webpack.prod.js 配置方式如下:

  1. module.exports = {
  2. plugins: [
  3. // 提取 CSS
  4. new MiniCssExtractPlugin({
  5. filename: "[hash].[name].css",
  6. }),
  7. ],
  8. }

配置 optimization.moduleIds,让公共包 splitChunks 的 hash 不因为新的依赖而改变,减少非必要的 hash 变动,webpack.prod.js 配置方式如下:

  1. module.exports = {
  2. optimization: {
  3. moduleIds: 'deterministic',
  4. }
  5. }

通过配置 contenthash/hash,浏览器缓存了未改动的文件,仅重新加载有改动的文件,大大加快加载速度。

6.3 CDN

上面已经介绍过啦;

7、总结

在小型项目中,添加过多的优化配置,作用不大,反而会因为额外的 loader、plugin 增加构建时间;

在加快构建时间方面,作用最大的是配置 cache,可大大加快二次构建速度。

在减小打包体积方面,作用最大的是压缩代码、分离重复代码、Tree Shaking,可最大幅度减小打包体积。

在加快加载速度方面,按需加载、浏览器缓存、CDN 效果都很显著。

8、本节代码

代码地址:https://gitee.com/linhexs/webpack5/tree/4.optimize/