Webpack5 常用优化总结

本文主要总结经常用到的一些代码性能优化、减小代码体积、提升webpack打包构建速度 等内容的方法。具体的实现可参考webpack官网查看相关示例。

注:如果读者还未接触过webpack,请先了解webpack的基本使用。

正文:

SourceMap ---- 提升开发体验

SourceMap源代码映射,是一个用来生成源代码与构建后代码一一映射的文件的方案。

使用webpack打包之后会生成一个与打包文件对应的.map文件,里面包含源代码和构建后代码每一行、每一列的映射关系。当构建后的代码报错时,其会通过.map文件将构建后代码中出错的位置映射到源代码出错的位置,从而让浏览器的报错提示的是源代码文件报错的位置信息,帮助开发人员快速定位。
解决方案: 可以通过设置devtool来控制如何生成source map。开发模式下我们一般使用cheap-module-source-map,优点:打包编译速度快,只包含行映射,缺点:没有列映射。生产模式下使用source-map,优点:包含行和列的映射,缺点:打包编译速度慢。

javascript 复制代码
// webpack.dev.js
module.exports = {
	mode: 'development',
	devtool: 'cheap-module-source-map'
}
// webpack.prod.js
module.exports = {
	mode: 'production',
	devtool: 'source-map'
}

提升构建打包速度

1. HotModuleReplacement(HMR)模块热替换

在webpack5中,热更新是webpack默认开启的。开发时只重新编译打包更新变化了的代码,不变的代码使用缓存,从而实现局部更新,而不是刷新整个页面。

javascript 复制代码
// webpack.config.js
module.exports = {
	devServer: {
		hot: true
	}
}
2. OneOf 规则数组 只使用第一个匹配规则

当在webpack配置文件中写了很多处理不同资源文件的loader时,资源文件会遍历所有loader进行解析处理,当使用了oneOf规则之后,资源文件一旦被某个loader处理了,就不会继续往下遍历,从而使打包速度更快。

javascript 复制代码
// webpack.config.js
module.exports = {
	module: {
		rules: [
			{
				oneOf: [
					{
						test: /\.css$/,
						use: ['style-loader', 'css-loader']
					},
					{
						test: /\.less$/,
						use: ['style-loader', 'css-loader', 'less-loader']
					},
					{
						test: /\.s[sc]ss$/,
						use: ['style-loader', 'css-loader', 'sass-loader']
					},
					{
						test: /\.styl$/,
						use: ['style-loader', 'css-loader', 'stylus-loader']
					},
					{
						test: /\.(png|jpe?g|gif|webp|svg)$/,
						type: 'asset',
						parser: {
							dataUrlCondition: {
								// 小于10KB的图片转base64
								maxSize: 10 * 1024
							}
						},
						generator: {
							filename: 'static/img/[hash:10][ext][query]'
						}
					},
					{
						test: /\.(ttf|woff2?|mp3|mp4|avi)$/,
						type: 'asset/resource',
						generator: {
						filename: 'static/media/[hash:10][ext][query]'
						}
					},
					{
						test: /.\.js$/,
						exclude: /node_modules/,
						loader: 'babel-loader'
					}
				]
			}
	
		]
	}
}
3. Include/Exclude

引入或排除某些文件,处理的文件更少,速度更快。例如开发时我们用到了第三方的库或者插件,那么这些文件是不需要编译就可以使用的,所以可以排除(exclude)对这些文件的处理。或者只处理(include)某个文件夹下面的源代码。二者选其一使用。

javascript 复制代码
// webpack.config.js
const path = require('path')
const ESlintPlugin= require('eslint-webpack-plugin')

module.exports = {
	module: {
		rules: [
			{
				test: /.\.js$/,
				// 排除node_modules下的文件 不作处理
				exclude: /node_modules/,
				loader: 'babel-loader'
			},
			// 或者
			{
				test: /.\.js$/,
				// 只处理src下的内容 其他文件不处理
				include: path.resolve(__dirname, './src'),
				loader: 'babel-loader'
			},
		]
	},
	plugins: [
		new ESlintPlugin([
			context: path.resolve(__dirname, './src'),
			exclude: /node_modules/, // 默认值
			//或者
			include: path.resolve(__dirname, './src'),
		])
	]
}
4. Cache 缓存

可以对eslint和babel处理的结果进行缓存,让后续打包速度更快。

javascript 复制代码
// webpack.config.js
const path = require('path')
const ESlintPlugin= require('eslint-webpack-plugin')

module.exports = {
	module: {
		rules: [
			{
				test: /.\.js$/,
				// 排除node_modules下的文件 不作处理
				exclude: /node_modules/,
				loader: 'babel-loader',
				options: {
					cacheDirectory: true, //  开启babel缓存
					cacheCompression: false // 关闭缓存文件压缩	
				}
			},
		]
	},
	plugins: [
		new ESlintPlugin([
			context: path.resolve(__dirname, './src'),
			exclude: /node_modules/, // 默认值
			cache: true, // 开启缓存
			cacheLocation: path.resolve(__dirname, './node_modules/.cache/eslintcache') // 定义缓存位置
		])
	]
}
5. Thead 多进程打包

当项目非常庞大的时候,打包速度越来越慢,主要是对js文件进行检查(eslint)、编译(babel)、压缩(terser),要提升运行速度可以开启多进程同时处理js文件。由于进程启动通信都是有开销的,所以只有在代码比较多的时候处理才有效果。

javascript 复制代码
npm i thread-loader --save-dev

使用时,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的 worker 池中运行。

在 worker 池中运行的 loader 是受到限制的。例如:

  • 这些 loader 不能生成新的文件。
  • 这些 loader 不能使用自定义的 loader API(也就是说,不能通过插件来自定义)。
  • 这些 loader 无法获取 webpack 的配置。
    每个 worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。同时会限制跨进程的数据交换。

请仅在耗时的操作中使用此 loader!

javascript 复制代码
// webpack.config.js
const os = require('os')
const ESlintPlugin= require('eslint-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin') // 内置插件

const threads = os.cups().length  // 获取CPU核数

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve('src'),
        use: [
          {
          	loader: "thread-loader",
          	options: {
          		works: threads	
          	}
          },
      	  {
			loader: "babel-loader"	
		  }
        ],
      },
    ],
  },
  plugins: [
	new ESlintPlugin([
		context: path.resolve(__dirname, './src'),
		exclude: /node_modules/, // 默认值
		cache: true, // 开启缓存
		cacheLocation: path.resolve(__dirname, './node_modules/.cache/eslintcache'), // 定义缓存位置
		threads, // 开启多进程和设置数量
	]),
	new TerserPlugin({ // 代码压缩
		parallel: threads //开启多进程和设置数量
	})
  ],
  // 压缩插件 第二种写法
  optimization: {
  	minimizer: [
  		new CssMinimizerPlugin(), // css文件压缩
  		new TerserPlugin({ // js代码压缩
			parallel: threads //开启多进程和设置数量
		})
  	]
  }
};

减小代码体积

1. Tree Shaking

当我们编写了很多工具函数或者引入了第三方库,可能在实际开发中只应用了其中一部分,那么在打包时这些未用到的代码就无须进行打包。Tree Shaking就帮我们做了这件事情。它可以移除JS上下文中的死代码,且其语法依赖于ESM,不支持CommonJS。

在Webpack中已经默认开启了此配置,所以开发者无需再进行配置。

2. @babel/plugin-transform-runtime

此插件可以对babel进行处理,让辅助代码单独生成到一个文件中,引入到编译后的文件中,而不是每个文件都生成辅助代码,从而减小打包后的体积。

javascript 复制代码
npm i @babel/plugin-transform-runtime -D
javascript 复制代码
// webpack.config.js

module.exports = {
	module: {
		rules: [
			{
				test: /.\.js$/,
				// 排除node_modules下的文件 不作处理
				exclude: /node_modules/,
				loader: 'babel-loader',
				options: {
					cacheDirectory: true, //  开启babel缓存
					cacheCompression: false, // 关闭缓存文件压缩
					plugins: ['@babel/plugin-transform-runtime'] // 减小代码体积
				}
			},
		]
	}
}
3. Image Minimizer 图片压缩

当项目中使用了很多本地图片,那么可以对图片进行压缩,减小图片体积,从而加快请求速度。如果项目中使用的是在线链接的图片,那么就不需要进行配置了。

javascript 复制代码
npm i image-minimizer-webpack-plugin imagemin -D

安装完上面两个依赖包之后还需要下载另外两种压缩方式的包,读者选择性下载。

一是无损压缩:

javascript 复制代码
npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo --save-dev

二是有损压缩:

javascript 复制代码
npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo --save-dev
javascript 复制代码
// webpack.config.js
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
const { extendDefaultPlugins } = require('svgo');

module.exports = {
	module: {
     rules: [
        {
          test: /\.(jpe?g|png|gif|svg)$/i,
          type: 'asset',
        },
      ],
    },
	plugins: [
		new ImageMinimizerPlugin({
	      minimizerOptions: {
	        plugins: [
	          ['gifsicle', { interlaced: true }],
	          ['jpegtran', { progressive: true }],
	          ['optipng', { optimizationLevel: 5 }],
	          [
	            'svgo',
	            {
	              plugins: extendDefaultPlugins([
	                {
	                  name: 'removeViewBox',
	                  active: false,
	                },
	                {
	                  name: 'addAttributesToSVGElement',
	                  params: {
	                    attributes: [{ xmlns: 'http://www.w3.org/2000/svg' }],
	                  },
	                },
	              ]),
	            },
	          ],
	        ],
	      },
	   }),
	]
}

优化代码性能

1. Code Split 代码分割

在进行打包时,会将所有的js文件打包到一个文件中,导致体积太大,加载速度慢。当使用了代码分割之后,生成多个js文件,渲染哪个页面就加载哪个js文件,这样就会减少资源的加载,速度就更快,从而提升性能。

使用:

一、多入口、多输出

javascript 复制代码
module.exports = {
	entry: { // 多个入口文件,打包时就会产生多个输出文件
		main: './main.js',
		app: './src/app.js'
	},
	optimization: {
		splitChunks: { // 代码分割配置
			chunks: 'all',  // 对所有模块都进行分割
			minSize: 20000, // 生成chunk的最小体积(以bytes为单位)
	        minRemainingSize: 0, // 类似minSize,最后确保提取的文件大小不能为0
	        minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
	        maxAsyncRequests: 30, // 按需加载时并行加载文件的最大数量
	        maxInitialRequests: 30, // 入口js文件最大并行请求数量
	        enforceSizeThreshold: 50000, // 超过50KB一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
	        cacheGroups: {  // 组,指哪些模块要打包到一个组
	          defaultVendors: { // 组名
	            test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
	            priority: -10, // 权重,数值越大权重越高
	            reuseExistingChunk: true, // 如果当前chunk包含已从主bundle中拆分出的模块,则它将被重用,而不是生成新的模块
	          },
	          default: { // 默认属性,没有就会使用上面的配置,此配置会覆盖上面的配置
	            minChunks: 2,
	            priority: -20,
	            reuseExistingChunk: true,
	          },
	        },
		}
	}
}

二、单入口,多输出

javascript 复制代码
module.exports = {
	entry: './main.js',
	optimization: {
		splitChunks: {
			chunks: 'all'
		}
	}
}
2. Preload / Prefetch 预加载 预获取

preload:使浏览器立即加载资源;prefetch:等待浏览器空闲时开始加载资源。它们只会加载资源,并不执行且都有缓存。Preload加载优先级要高于Prefetch。

javascript 复制代码
npm install --save-dev @vue/preload-webpack-plugin
javascript 复制代码
const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin');

module.exports = {
	plugins: [
		new PreloadWebpackPlugin({
			rel: 'preload', // 预加载的rel
            as: 'script', // 预加载的资源类型
            include: 'allChunks' // 预加载的文件范围	
			// 或者使用 prefetch
			rel: 'prefetch'
		})
	]
}
3. Network Cache

对输出资源的文件更好的命名,可以做好缓存,提升性能。只对修改的文件打包之后改动文件名,其它文件不因引入改动的文件而改动文件名,可以更好的做到缓存。

javascript 复制代码
module.exports = {
	optimization: {
		runtimeChunk: {
			name: entrypoint => `runtime~${entrypoint.name}.js`
		}
	}	
}
4. Core-js 解决兼容性

例如一些ES6的新语法,babel无法处理,例如async函数、promise对象、数组的一些方法等等。所以可以使用core-js专门处理ES6及以上的语法。更好的适配老款浏览器的兼容性。

javascript 复制代码
npm i core-js

安装好之后在主入口引入即可

javascript 复制代码
import 'core-js'
5. PWA 渐进式网络应用程序(progressive web application - PWA)

当网络断开时,就无法访问Web应用了。为了提供离线访问效果,我们可以引入PWA,内部是通过Service Works技术实现的。可以将所有资源缓存到ServiceWork中,当离线时依旧可以访问。

javascript 复制代码
npm install workbox-webpack-plugin --save-dev
javascript 复制代码
const WorkboxPlugin = require('workbox-webpack-plugin');

  module.exports = {
    entry: {
      app: './src/index.js',
      print: './src/print.js',
    },
    plugins: [
      new HtmlWebpackPlugin({
       title: 'Output Management',
       title: 'Progressive Web Application',
      }),
     new WorkboxPlugin.GenerateSW({
       // 这些选项帮助快速启用 ServiceWorkers
       // 不允许遗留任何"旧的" ServiceWorkers
       clientsClaim: true,
       skipWaiting: true,
     }),
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
  };

最后,需要在主入口文件中注册Service Worker才能生效:

main.js

javascript 复制代码
 if ('serviceWorker' in navigator) {
   window.addEventListener('load', () => {
     navigator.serviceWorker.register('/service-worker.js').then(registration => {
       console.log('SW registered: ', registration);
     }).catch(registrationError => {
       console.log('SW registration failed: ', registrationError);
     });
   });
 }
相关推荐
liangshanbo121512 小时前
Webpack 文件指纹面试题整理
前端·webpack·node.js
liangshanbo121513 小时前
Webpack 常见 Plugin 面试题
前端·webpack·node.js
liangshanbo121514 小时前
Webpack Tree Shaking 面试题整理
前端·webpack·node.js
陪我一起学编程16 小时前
vue3笔记
前端·webpack·vue3·vite·脚手架·vue3笔记·组件式
liangshanbo121517 小时前
Webpack的分包策略面试题
前端·webpack·node.js
liangshanbo12152 天前
面试题:如何优化 Webpack 的打包速度?
前端·webpack·node.js
晚风醉蝶4 天前
webpack 模块提取
前端·webpack·node.js·ast·逆向分析
晚风醉蝶14 天前
保姆级教程:用 AST 自动提取 webpack / rspack 打包后的模块
webpack·web逆向
guwentian20 天前
Rolldown vs esbuild vs Webpack:Rust 打包器大战我们该怎么选
前端·webpack·rust·esbuild
飞翔的熊blabla22 天前
# Webpack 5 + Jenkins 持久化缓存实战:前端构建从 288 秒降到 30 秒>
前端·webpack·jenkins