原理
打包结果分析
- 就是一个立即执行函数
- 函数内容: 声明了一个webpack_require, 然后执行
//合并两个模块
// ./src/a.js
// ./src/index.js
(function (modules) {
var moduleExports = {}; //用于缓存模块的导出结果
//require函数相当于是运行一个模块,得到模块导出结果
function __webpack_require(moduleId) { //moduleId就是模块的路径
if (moduleExports[moduleId]) {
//检查是否有缓存
return moduleExports[moduleId];
}
var func = modules[moduleId]; //得到该模块对应的函数
var module = {
exports: {}
}
func(module, module.exports, __webpack_require); //运行模块
var result = module.exports; //得到模块导出的结果
moduleExports[moduleId] = result; //缓存起来
return result;
}
//执行入口模块
return __webpack_require("./src/index.js"); //require函数相当于是运行一个模块,得到模块导出结果
})({ //该对象保存了所有的模块,以及模块对应的代码
"./src/a.js": function (module, exports) {
eval("console.log(\"module a\")\nmodule.exports = \"a\";\n //# sourceURL=webpack:///./src/a.js")
},
"./src/index.js": function (module, exports, __webpack_require) {
eval("console.log(\"index module\")\nvar a = __webpack_require(\"./src/a.js\")\na.abc();\nconsole.log(a)\n //# sourceURL=webpack:///./src/index.js")
}
});
css-loader 原理
- 转换前的字符串
`
body {
color: red;
}
`
- 转换后的字符串
`
var style = document.createElement("style")
style.innerHTML = 'body{color: red}'
document.head.appendChild(style)
`
- css-loader.js
module.exports = function (sourceCode) {
var code = `
var style = document.createElement("style")
style.innerHTML = ${sourceCode}
document.head.appendChild(style)
`;
return code;
}
img-loader原理
- 将二进制的图片内容 --> 转成base64的字符串
- img-loader.js
function loader(buffer) { //给的是buffer
console.log("文件数据大小:(字节)", buffer.byteLength);
var base64 = getBase64(buffer)
return `module.exports = ${base64}`;
}
loader.raw = true; //该loader要处理的是原始数据
module.exports = loader;
function getBase64(buffer) {
return "data:image/png;base64," + buffer.toString("base64");
}
性能优化: 打包体积: (生产环境)
压缩: js压缩
一键开启js压缩
minimize: true- 默认就会使用
terser-webpack-plugin来压缩代码 - 生产环境默认值为
true, 生产环境下 自动压缩代码 ,减少文件体积,提升加载性能 - 开发环境默认值为
false, 开发环境下 不压缩代码 ,便于调试
- 默认就会使用
module.exports = {
optimization: {
minimize: true, // <------------
},
entry: path.resolve(__dirname, "../src/main.ts"),
output: { ... },
resolve: { ... },
plugins: [...],
module: {...},
};
自定义js压缩
- 使用
TerserPlugin开启自定义压缩 - 为什么要自定义压缩?
- minimize: true 就像一辆「出厂配置的汽车」,能满足基本的代步需求;
- 而显式配置 TerserPlugin 就像「改装汽车」,可以根据你的具体需求(比如性能、安全性、个性化)进行定制,让它更适合你的项目!
- 当只设置 minimize: true 而不自定义 TerserPlugin 时:
- webpack 会使用 TerserPlugin 的 默认配置 来压缩代码
- 默认配置是比较保守的,主要是基本的体积压缩
- 不会删除 console.log 、 debugger 等调试语句
- 会保留一些注释
- 通过自定义 TerserPlugin ,你可以:
- 删除调试代码 :如 console.log 、 debugger
- 清理注释 :进一步减小文件体积
- 自定义压缩策略 :根据项目需求调整压缩强度
- 提升安全性 :压缩后的代码更难被反编译
const TerserPlugin = require("terser-webpack-plugin");
// 自定义js压缩
const terserPlugin = new TerserPlugin({
terserOptions: {
format: {
comments: false, // 不保留注释
},
compress: {
drop_console: true, // 删除console.log
drop_debugger: true, // 删除debugger
pure_funcs: ['console.log','console.error','console.info'] // 删除console相关打印语句
}
},
extractComments: false, // 不将注释提取到单独的文件中
})
webpackConfig.optimization = {
minimizer: [terserPlugin] // <<------
}
压缩: css压缩
- minimize: 默认压缩器
- minimizer 是默认压缩器
- 默认就会使用 terser-webpack-plugin 来压缩代码
- 生成环境默认值true
- 开发环境默认值false
- minimizer: 自定义压缩器
- minimizer 是 webpack 中用于配置代码压缩工具的核心配置项。
- 当你自定义压缩代码时,webpack 会 完全替换默认的压缩器配置 ,而不再使用内置的默认压缩器。
- 引号包裹的三个点: "..."
- 引号包裹的三个点 "..." 被称为 "webpack 的默认压缩器占位符"
- 它的作用是 保留 webpack 原本默认的压缩器配置 ,同时允许你添加自定义的压缩器
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
webpackConfig.optimization = {
minimize: true, // 默认压缩器, 下面的 minimizer 会让它失效
minimizer: [
new CssMinimizerPlugin(), // 分离css
// minimize 是默认压缩器
// 当你设置 minimizer 数组时,会完全替换默认的压缩器
// "..." 表示: 依旧使用默认压缩器压缩js
"..."
]
}
压缩: treeShaking
react实践
import {Button} from 'antd
vue实践
- AutoImport 和 Components 插件是按需加载的具体实现,它们通过自动分析代码并仅导入使用的功能,从而减少了最终打包体积。这一过程与 Tree Shaking 相辅相成,共同提升应用性能。
module.exports = {
entry: path.resolve(__dirname, "../src/main.ts"),
output: { ... },
resolve: { ... },
plugins: [
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
module: {...},
};
AutoImport作用
plugins: [
AutoImport({
// 按需加载ElementPlu的 ElMessage
resolvers: [ElementPlusResolver()],
})
],
- AutoImport 是 Vue 3 + Element Plus 项目中的 必备优化工具 ,它通过自动导入常用函数和 API,显著提高了开发效率,减少了代码冗余,是现代前端工程化的重要实践之一。
// 无需import 自动导入 ref, ElMessage
// 都是按需加载的
export default {
setup() {
const count = ref(0) // ref 自动导入自 vue
ElMessage.success('操作成功') // ElMessage 自动导入自 element-plus
return { count }
}
}
Components作用
plugins: [
Components({
// 按需加载ElementPlu
resolvers: [ElementPlusResolver()],
}),
],
使用后效果, 不需导入直接使用即可, 并且是按需加载的
<template>
<el-button type="primary" @click="showMessage">点我</el-button>
</template>
<script setup>
// 无需导入
</script>
与传统按需加载的区别
- 使用 AutoImport 和 Components:
- 无需手动导入,插件自动完成
- 自动处理样式
- 更简洁,更不容易出错
- 传统按需加载:
- 需要手动导入每个组件和对应的样式
- 容易出错,维护麻烦
import { ElButton, ElTable } from 'element-plus'
import 'element-plus/es/components/button/style/css'
import 'element-plus/es/components/table/style/css'
app.component('ElButton', ElButton)
app.component('ElTable', ElTable)
压缩: GZIP
- Nginx动态压缩: Nginx帮我们压缩
- Nginx静态压缩: 提前压缩好交给Nginx
- 如果需要压缩的文件太多, 服务器压力太大,我们就提前压缩好
- 这里我们使用插件: CompressionPlugin, 提前压缩好
const gzipPlugin = new CompressionPlugin({
algorithm: "brotliCompress", // 压缩算法,默认gzip,也可以是brotliCompress
test: /\.(js|css)(\?.*)?$/i, //需要压缩的文件正则
threshold: 1024, //文件大小大于这个值时启用压缩
deleteOriginalAssets: true //压缩后是否删除原文件
})
webpackConfig.plugin = [gzipPlugin]
分包: 分离css
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const isProduction = process.env.NODE_ENV === "production";
const getStyleLoaders = () => {
return [
// 生产环境才分离css, 开发环境不分离
isProduction ? MiniCssExtractPlugin.loader : "style-loader",
"css-loader",
"postcss-loader",
].filter(Boolean);
};
const cssLoader = {
test: /\.css$/i,
exclude: [/\.module\.css$/],
use: getStyleLoaders(),
}
// 分离的css文件位置以及名称
const miniCssExtractPlugin = new MiniCssExtractPlugin({
filename: "css/[name].[contenthash:6].css",
})
const webpackConfig.plugins = [miniCssExtractPlugin]
webpackConfig.module.rules = [cssLoader]
分包: js懒加载,预加载
{
path: "/",
name: "Home",
component: () => import(
/* webpackChunkName: "HomeView" */
/* webpackPreload: true */ // 首页优先加载
"@/views/HomeView.vue"),
},
{
path: "/user",
name: "User",
component: () => import(
// 给分包出去的文件取名
/* webpackChunkName: "UserView" */
// 预加载, 就是空闲时加载
/* webpackPrefetch: true */
"@/views/UserView.vue"),
},
{
path: "/image",
name: "Image",
component: () => import(
/* webpackChunkName: "ImageView" */
/* webpackPrefetch: true */
"@/views/ImageView.vue"),
},
分包: 分离运行时代码
const baseConfig = require('./webpack.base.js');
const { merge } = require('webpack-merge');
const prodConfig = {
mode: 'production',
plugins: [...],
optimization: {
...
chunkIds: "named", // 给分离的包自动取名
runtimeChunk: 'single', // 分离运行时代码<<------------
...
},
}
module.exports = merge(baseConfig, prodConfig);
什么是运行时代码:
- webpack产生的代码叫运行时代码
为什么要设置 runtimeChunk: 'single'
- 默认行为 :运行时代码通常会嵌入到每个入口文件中 问题
- 导致代码重复(多个入口会有相同的运行时代码)
- 缓存效果差(只要运行时代码变化,所有入口文件都需要重新加载)
- 设置为 'single' 后的好处 :
- 减少代码体积:运行时代码只存在于一个文件中
- 优化缓存:运行时代码与业务代码分离,业务代码变化时不会影响运行时代码的缓存
- 提升构建效率:运行时代码单独提取,便于 webpack 进行更高效的代码分析和处理
分包: 自定义
- webpack.production.js
const baseConfig = require('./webpack.base.js');
const { merge } = require('webpack-merge');
const prodConfig = {
mode: 'production',
plugins: [...],
optimization: {
...
...
splitChunks: {...} // 自定义分包<---------
},
}
module.exports = merge(baseConfig, prodConfig);
splitChunks详细配置- element, echarts, 会被单独打包
- 大于160KB 的大库(如 Vue 3、React 等)会被 lib 优先处理
- 20KB - 160KB 的中等库会被 module 处理
- 小于20KB 的小库会被 vendors 合并
// 将node_modules分离出来统一打包
const vendors = {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: 10,
reuseExistingChunk: true,
// chunks:'async'
}
// 将element单独打包
const element = {
name: 'chunk-element',
test: /[\\/]node_modules[\\/]element(.*)/,
priority: 20,
reuseExistingChunk: true,
}
// 将echarts 单独打包
const echarts = {
name: 'chunk-echarts',
test: /[\\/]node_modules[\\/]echarts|zrender(.*)/,
priority: 20,
reuseExistingChunk: true,
}
// 将共用代码单独打包: (如: tools.js)
const commons = {
name: 'chunk-commons',
minChunks: 2, // 为了演示效果,只要引用了2次以上就会打包成单独的js
priority: 5,
minSize: 0, // 为了演示效果,设为0字节,实际情况根据自己的项目需要设定
reuseExistingChunk: true,
}
// 将 node_modules 下大于160kb的库单独打包
const lib = {
test(module) {
// 如果模块大于160kb,并且模块名字中包含node_modules, 就会被单独打包到一个文件中
return module.size() > 160000
&& module.nameForCondition()
&& module.nameForCondition().includes('node_modules')
},
name(module) {
const packageNameArr = module.context.match(/[\\/]node_modules[\\/]\.pnpm[\\/](.*?)(\/|$)/)
const packageName = packageNameArr ? packageNameArr[1] : ''
return `chunk-lib.${packageName.replace(/@/g,"")}`;
},
priority: 20,
minChunks: 1,
reuseExistingChunk: true,
}
// 将 node_modules 的库单独打包(默认设置: 大于20kb才会单独打包)
const module = {
test: /[\\/]node_modules[\\/]/,
name(module) {
const packageNameArr = module.context.match(/[\\/]node_modules[\\/]\.pnpm[\\/](.*?)(\/|$)/)
const packageName = packageNameArr ? packageNameArr[1] : ''
return `chunk-module.${packageName.replace(/@/g,"")}`;
},
priority: 15,
minChunks: 1,
reuseExistingChunk: true,
}
webpackConfig.optimization.splitChunks = {
chunks: 'all',
cacheGroups: {
vendors,// 将node_modules分离出来统一打包
element,// 将element单独打包
echarts,// 将echarts 单独打包
commons,// 将共用代码单独打包: (如: tools.js)
lib,// 将 node_modules 下大于160kb的库单独打包
module// 将 node_modules 下大于20kb的库单独打包
}
}
CDN: 外部依赖
- 使用CDN 进一步减小打包体积
- externals 是 webpack 的核心配置项之一,用于 声明"外部依赖" ,告诉 webpack 在打包时不要将这些依赖项包含到最终的 bundle 中,而是期望它们在运行时从外部环境(如 CDN、全局变量等)获得。
- 开发环境 :不配置 externals ,直接从 node_modules 加载依赖,保持开发体验和调试便利性
- 生产环境 :配置 externals ,使用 CDN 加载大型依赖,优化打包体积和加载性能
- 这样既保证了开发效率,又能在生产环境获得性能优化的好处。
webpack.production.js
const prodConfig = {
mode: 'production',
plugins: [...],
optimization: {...},
externals: {
vue: 'Vue',
"vue-router": "VueRouter",
"element-plus": "ElementPlus",
"@vueuse/core": "VueUse",
echarts: 'echarts',
"vue-echarts": "VueECharts",
}
}
module.exports = merge(baseConfig, prodConfig);
这个配置对象的结构是 { [包名]: [全局变量名] } ,表示:
- 当你的代码中引用 vue 时,webpack 会假设运行时环境中存在一个全局变量 Vue
- 当你的代码中引用 vue-router 时,运行时环境中需要存在全局变量 VueRouter
- 其他同理...
记得在模板中引入CDN
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="<%= htmlWebpackPlugin.options.BASE_URL %>favicon.ico" type="image/x-icon">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= htmlWebpackPlugin.options.title %></title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/element-plus@2.3.12/dist/index.min.css"
/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vue-echarts@6.6.1/dist/csp/style.min.css">
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@4.2.4/dist/vue-router.global.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@vueuse/shared@10.4.1/index.iife.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@vueuse/core@10.4.1/index.iife.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/element-plus@2.3.12/dist/index.full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-echarts@6.6.1/dist/index.umd.min.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
性能优化: 打包速度: (开发环境优化)
缓存: 介绍
- 在现代 webpack 项目中, 多层级缓存策略 是常见的最佳实践:
- 第一层 :webpack 核心缓存(管理整个构建流程)
- 第二层 :各个 loader 缓存(如 babel-loader、ts-loader 等)
- 第三层 :各个插件缓存(如 ESLintPlugin、TerserPlugin 等)
缓存: 第1层: 核心缓存
type: "filesystem"缓存到硬盘- config: [__filename] 的核心含义:
- config 字段 :表示 webpack 配置文件的依赖
- __filename :Node.js 内置变量,表示当前脚本文件的完整路径(即 webpack.base.js )
- 整体功能 :当 webpack.base.js 文件内容发生任何修改时,缓存会自动失效,触发一次全新的构建
const cache = {
type: "filesystem",
buildDependencies: {
config: [__filename],
},
}
module.exports = {
cache, // <<---------------
entry: path.resolve(__dirname, "../src/main.ts"),
output: { ... },
resolve: { ... },
plugins: [ ... ],
module: { ... },
};
缓存: 第2层: loader缓存
- babelLoader 开启缓存
- 我们可以基于一种假设:如果某个文件内容不变,经过相同的loader解析后,解析后的结果也不变
- 于是,可以将loader的解析结果保存下来,让后续的解析直接使用保存的结果
const babelLoader = {
test: /\.m?jsx?$/,
exclude: /node_modules/,
use: [
{ loader: "thread-loader" }, // 多线程处理
{
loader: "babel-loader",
options: {
cacheDirectory: true, // 开启缓存
cacheCompression: false, // 关闭缓存压缩
}
}
],
}
缓存: 第3层: 插件缓存
- eslint 开启缓存
new ESLintPlugin({
extensions: ["js", "ts", "vue", "jsx", "tsx"],
exclude: "node_modules",
context: path.resolve(__dirname, "../src"),
cache: true, // 开启缓存
// 缓存目录
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/.eslintcache"
),
}),
缩小范围: 减少AST模块解析
- 模块解析是指: 构建AST语法树,分析文件依赖
- jquery 不依赖任何第三方库, 所以不需要解析, 从而提高打包速度
module.exports = {
mode: "development",
devtool: "source-map",
module: {
noParse: /jquery/
}
}
缩小范围: loader
- 例如: 排除lodash
- lodash 本来就是用es5写的
- 不需要babel-loader 转换语法
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /lodash/, // <-------
use: "babel-loader"
}
]
}
}
- exclude: 排除第三方库
- node_modules
- 第三方库90%都是经过webpack打过包的
- 不需要babel-loader 转换语法
// 写法一: babel-loader 不处理 node_modules
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/, // <-------
use: "babel-loader"
}
]
}
}
// 写法二: babel-loader 只处理 src 下的文件
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: /src/, // <-------
use: "babel-loader"
}
]
}
}
- include: 缩小图片处理范围
- 缩小图片处理范围
- 通过 include 指定要处理的文件夹
const fileLoader = {
test: /\.(png|jpe?g|gif|webp|avif)(\?.*)?$/,
// 只处理这个文件夹里的图片
include: path.resolve(__dirname, "../src/assets/images"),
// webpack5通用资源处理模块,默认8kb以下的资源会被转换为base64
type: "asset",
parser: {
dataUrlCondition: {
// 10kb以下的资源会被转换为base64
maxSize: 10 * 1024,
},
},
generator: {
// 所有匹配的图片资源都会被输出到 dist/img/ 目录下
filename: "img/[name].[contenthash:6][ext]",
},
},
多线程: thread-loader
- thread-loader 是 webpack 的一个插件,它可以将昂贵的 loader 处理(如 babel 转译、TypeScript 编译)分配到多个工作线程中并行执行,从而提高构建速度。
use: [
{ loader: "thread-loader" },
{ loader: "babel-loader" }
]
-
什么时候应该开启多线程
-
- 项目规模较大时, 多线程可以显著减少构建时间
-
- CPU 核心较多时
-
- 处理复杂转译逻辑时(当需要处理大量第三方库代码时)
-
- 生产构建环境适合开启, 因为生产环境不用频繁打包
-
-
什么时候不应该开启多线程
-
- 项目规模较小时
-
- CPU 资源有限时
-
- 处理简单任务时
-
- 开发模式下不宜开启, 因为开发环境需要频繁打包
-
const babelLoader = {
test: /\.m?jsx?$/,
exclude: /node_modules/,
use: [
{ loader: "thread-loader" }, // 多线程处理
{
loader: "babel-loader",
options: {
cacheDirectory: true,
cacheCompression: false,
}
}
],
}
文件指纹: 开发环境下不用指纹
filename: isProduction ? "js/[name].[contenthash:6].js" : 'js/[name].js',
output: {
path: path.resolve(__dirname, "../dist"), // 打包后的目录
filename: isProduction ? "js/[name].[contenthash:6].js" : 'js/[name].js', // 打包后的文件名
chunkFilename: isProduction ? "js/[name].[contenthash:8].js" : 'js/[name].chunk.js', // 代码拆分后的文件名
// assetModuleFilename: isProduction ? "img/[name].[contenthash:6][ext]" : 'img/[name][ext]', // 资源模块打包后的文件名
clean: true, // 清除上一次打包的文件
publicPath: "/", // 打包后的资源的访问路径前缀
},