背景
历史原因前一次升级是将webpack1.x版升级到3.x, 主要使用happyPack+dllPlugins; 具体配置可前往:www.yuque.com/donsoft/qkn...;
项目模块数庞大,重构可能性不大 ,在不影响业务开发及devops正常流程,索性在3.x的基础上升级到5
官方资料
源代码仓库:github.com/webpack/web...
源码压缩包下载:📎webpack-main.zip
构建工作流程:
- 初始化参数:取配置文件和shell脚本参数并合并
- 开始编译:用上一步得到的参数初始化compiler对象,执行run方法开始编译
- 确定入口:根据配置中的entry,确定入口文件
- 编译模块:从入口文件出发,递归遍历找出所有依赖模块的文件
- 完成模块编译:使用loader转译所有模块,得到转译后的最终内容和依赖关系
- 输出资源:根据入口和模块依赖关系,组装成一个个chunk,加到输出列表
- 输出完成:根据配置中的output,确定输出路径和文件名,把文件内容写入输出目录(默认是dist)
前置说明:务必注意
-g/--save/--save-dev
的区分
全局安装:npm install webpack -g
可以通过npm config list
命令 查看全局安装的包
局部安装:npm install webapck --save-dev 或 npm install webpack -D
npm install -g moduleName 命令
-
安装模块到全局,不会在项目node_modules目录中保存模块包。
-
不会将模块依赖写入devDependencies或dependencies 节点。
-
运行 npm install 初始化项目时不会下载模块。
npm install -save moduleName 命令
-
安装模块到项目node_modules目录下
-
会将模块依赖写入dependencies 节点。
-
运行 npm install 初始化项目时,会将模块下载到项目目录下。
-
运行npm install --production或者注明NODE_ENV变量值为production时,会自动下载模块到node_modules目录中。
npm install -save-dev moduleName 命令
-
安装模块到项目node_modules目录下。
-
会将模块依赖写入devDependencies 节点。
-
运行 npm install 初始化项目时,会将模块下载到项目目录下。
特别说明:devDependencies与dependencies的区别
开发环境(devDependencies) :
devDependencies下列出的模块,是开发时用的依赖项,像一些模块打包器,比如webpack,我们用它打包js文件,只用于开发环境,不会被部署到生产环境。
生产环境(dependencies):
dependencies下列出的模块,则是生产环境中需要的依赖,即正常运行该包时所需要的依赖项,是需要部署到生产环境的。如:react、antd等
在安装包依赖时请务必注意区分。
一、开始工作
优化开发体验、加快编译速度、减小打包体积、加快加载速度,代码分割,持久化缓存,模块联邦等 维度,介绍如何对 webpack 项目进行配置最优。
依赖的 webpack 版本信息如下:
diff
- node v16.13.1
- npm v7.x.x
- webpack-cli\@4.7.2
- webpack\@5.69.1
二、优化工具
安装以下 webpack 插件,帮助我们分析优化效率:
scss
- progress-bar-webpack-plugin | webpackbar:// 查看编译进度;
- speed-measure-webpack-plugin: // 查看编译速度;
- webpack-bundle-analyzer:// 打包体积分析
1. 进度条工具
通过 progress-bar-webpack-plugin 插件查看编译进度,方便在npm run dev 时查看编译情况。
安装:
css
npm i progress-bar-webpack-plugin -D
npm i webpackbar -D
webpack.common.js 配置方式如下:
javascript
//进度百分比添加了加粗和绿色高亮态样式
const chalk = require('chalk')
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
module.exports = {
plugins: [
// 进度条
new ProgressBarPlugin({
format: ` :msg [:bar] ${chalk.green.bold(':percent')} (:elapsed s)`
})
],
}
//简易配置
const WebpackBar = require('webpackbar')
const ProgressBarWebpackPlugin = require('progress-bar-webpack-plugin')
module.exports = {
// ...
plugins: [
new WebpackBar(),
new ProgressBarWebpackPlugin(),
],
包含内容、进度条、进度百分比、消耗时间,进度条效果如下:
本次配置中使用WebpackBar的配置
2. 编译速度分析
优化 webpack 构建速度,首先需要知道是哪些插件、哪些 loader 耗时长,方便有针对性的优化。
通过 speed-measure-webpack-plugin 插件进行构建速度分析,可以看到各个 loader、plugin 的构建时长,可针对耗时 loader、plugin 进行优化。
安装:
css
npm i speed-measure-webpack-plugin -D
webpack.dev.js 配置方式如下:
ini
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
// ...
})
// 简易配置(此次配置中使用此方案)
plugiins:[
new SpeedMeasurePlugin();
]
包含各工具的构建耗时,效果如下:
三、提升开发体验
1. 自动热新
自动更新 指的是,在开发过程中,修改代码后,无需手动再次编译,可以自动编译代码更新编译后代码的功能。
webpack 提供了以下几种可选方式,实现自动更新功能:
vbscript
webpack's Watch Mode
webpack-dev-server 4.x; webpack-server 5.x**
webpack-dev-middleware
webpack 官方推荐的方式是 webpack-dev-server[5.x版本以下,建议使用react-hot-loader]
5.X版本中使用@pmmmwh/react-refresh-webpack-plugin
侵入性更少 ,配置简单
所需要版本信息:
Dependency | Version |
---|---|
react | 16.13.0+ or 17.x |
react-dom | 16.13.0+ or 17.x |
react-refresh | 0.10.0+ |
webpack | 4.46.0+ or 5.2.0+ |
这是针对开发环境的优化,修改 webpack.dev.js 配置。
2. 热更新
热更新 指的是,在开发过程中,修改代码后,仅更新修改部分的内容,无需刷新整个页面。
2.1 修改 webpack-dev-server 配置
使用 webpack 内置的 HMR 插件,更新 webpack-dev-server 配置。
webpack.dev.js 配置方式如下:
arduino
module.export = {
devServer: {
contentBase: path.resolve(__dirname, "dist"),
port: "4099",
// proxy: process.env.ENV === 'loc' ? { //反向代理,根据需求自行修改
// "/api": {
// target: "http://127.0.0.1:3001",
// pathRewrite: {
// "^/api": ""
// }
// }
// }:{},
open: true,
host: 'localhost',
hotOnly: false, // 页面构建失败不刷新页面
hot: true, //让webpackDevServer开启热更新功能
},
}
2.2 引入 react-refresh-webpack-plugin
使用 react-refresh-webpack-plugin 热更新 react 组件。
安装:
bash
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh
webpack.dev.js 配置方式如下:
ini
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
plugins: [
new webpack.HotModuleReplacementPlugin(),
new ReactRefreshWebpackPlugin(),
]
}
遇到的问题:
配置了 SpeedMeasurePlugin 插件后后,热更新无效了,会提示 runtime is undefined。
解决方案:
仅在需要分析构建速度时打开 SpeedMeasurePlugin 插件,否则注释掉;此次配置将注释它
最终效果:
更新 react 组件代码时,无需刷新页面,仅更新组件部分。
四、提升构建速度
1. 更新版本
1.1 webpack 版本
使用最新的 webpack 版本,通过 webpack 自身的迭代优化,来加快构建速度。比如增加的持久化缓存
www.jianshu.com/p/dfd794119...
1.2 包管理工具版本
将 Node.js 、package 管理工具(例如 npm / yarn / pnpm)更新到最新版本,也有助于提高性能。较新的版本能够建立更高效的模块树以及提高解析速度。本次演示依赖的版本信息如下:
- webpack@5.69.1
- node@16.x.x
- npm@7.x.x
同时约束只能使用npm的原因也是借助package-lock.json 强大缓存的原因
2. 缓存
2.1 cache
通过配置 webpack 持久化缓存
cache: filesystem,来缓存生成的 webpack 模块和 chunk,改善构建速度。
简单来说,通过 cache: filesystem 可以将构建过程的 webpack 模板进行缓存,大幅提升二次构建速度、打包速度,
当构建突然中断,二次进行构建时,可以直接从缓存中拉取,可提速 90% 左右。
2.2 dll 已废弃
在 webpack 官网构建性能 关于 dll 的介绍:
dll 可以为更改不频繁的代码生成单独的编译结果。可以提高应用程序的编译速度。
webpack5 开箱即用的持久缓存是比 dll 更优的解决方案。
dll+happypack方案详见:www.yuque.com/donsoft/qkn...
2.3 cache-loader 已废弃
3. 减少 loader、plugins
每个的 loader、plugin 都有其启动时间。尽量少地使用工具,将非必须的 loader、plugins 删除。
3.1 指定 include
为 loader 指定 include,减少 loader 的查找范围,。
webpack 构建性能文档
rule.exclude 可以排除模块范围,也可用于减少 loader 应用范围
webpack.common.js 配置方式见壳工程:
定义 loader 的 include 后,构建时间将减少 12%,效果如下:
3.2 管理资源
使用 webpack 资源模块 (asset module) 代替旧的 assets loader(如 file-loader/url-loader/raw-loader/json-loader/ 等),减少 loader 配置数量。
webpack.common.js 配置方式如下:
bash
{
test: /.(png|gif|bmp|jpg)$/,
type: 'asset/resource',
},
{
test: /.(woff(2)?|eot|ttf|otf|svg|)$/,
type: 'asset/inline',
},
4. 优化 resolve 配置
resolve 用来配置 webpack 如何解析模块,可通过优化 resolve 配置来覆盖默认配置项,减少解析范围。
4.1 alias
alias 可以创建 import 或 require 的别名,用来简化模块引入。
less
alias: {
pages: path.join(__dirname, 'src/pages'),
widget: path.join(__dirname, 'src/widget'),
router: path.join(__dirname, 'src/router'),
utils: path.join(__dirname, 'src/utils'),
assets: path.join(__dirname, 'src/assets'),
themes: path.join(__dirname, 'src/themes'),
config: path.join(__dirname, 'src/config'),
'yx-widget': path.join(__dirname, 'src/yx-widget'),
},
4.2 extensions
extensions 表示需要解析的文件类型列表。
根据项目中的文件类型,定义 extensions,以覆盖 webpack 默认的 extensions,加快解析速度。
由于 webpack 的解析顺序是从左到右,因此要将使用频率高的文件类型放在左侧,如下我将 jsx 放在最左侧。
perl
extensions: ['.jsx', '.web.js', '.js', '.json', '.css'], //去掉['',]空
4.3 modules
modules 表示 webpack 解析模块时需要解析的目录。
指定目录可缩小 webpack 解析范围,加快构建速度。
csharp
modules: [path.join(__dirname, 'src'), 'node_modules'], // 仅在项目目录下查找node_modules,找不到则不会往上层找(全局)
4.4 symlinks
如果项目不使用 symlinks(例如 npm link 或者 yarn link),可以设置 resolve.symlinks: false,减少解析工作量。
4.5 mainFiles
和npm link 类似,所有模块统一从package.json文件中找入口文件, 找不到则直接找项目根目录入口文件
vbnet
mainFields: ['loader', 'main'],
mainFiles: ['index.js'],
webpack.common.js 配置方式:
css
resolve: {
alias: {
pages: path.join(__dirname, 'src/pages'),
widget: path.join(__dirname, 'src/widget'),
router: path.join(__dirname, 'src/router'),
utils: path.join(__dirname, 'src/utils'),
assets: path.join(__dirname, 'src/assets'),
themes: path.join(__dirname, 'src/themes'),
config: path.join(__dirname, 'src/config'),
"yx-widget": path.join(__dirname, 'src/yx-widget'),
},
modules: [path.join(__dirname, 'src'), 'node_modules'],
extensions: ["", ".js", ".jsx", '.less', '.css'],//配置文件后缀缩小查找范围
mainFields: ['loader', 'main'], // 从package.json文件中找入口文件。
mainFiles: ['index.js'],
symlinks: false,
},
resolveLoader: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
// 其中 __dirname 表示当前工作目录,也就是项目根目录
modules: [path.resolve(__dirname, 'node_modules')],
},
// cdn配置后,index.html 引入cdn链接
externals: {
// 'react': 'React',
// 'react-dom': 'ReactDOM',
// 'react-router-dom': 'ReactRouterDOM'
},
优化 resolve 配置后,构建时间将减少 1.5%
5. 多进程
测试效果中 babel-loader 的构建时间占据了整个构建过程的 50%,通过多进程将Loader放在一个独立的 worker 池中运行,就不会阻碍其他 loader 的构建,可以加快构建速度。
5.1 thread-loader
通过 thread-loader ****将耗时的 loader 放在一个独立的 worker 池中运行,加快 loader 构建速度。
安装:npm i -D thread-loader
官方配置:
webpack.js.org/loaders/thr...
注意:如果使用的是sass
webpack 官网 提到 node-sass 中有个来自 Node.js 线程池的阻塞线程的 bug。当使用 thread-loader 时,需要设置 workerParallelJobs: 2。
thread-loader会对其后面的loader(这里是babel-loader)开启多进程打包。
进程启动大概为600ms,进程通信也有开销。(启动有一定开销,不要滥用)
只有工作消耗时间比较长,才需要多进程打包
thread-loader必须最后执行,再次说明loader是从下往上,从右往左的执行顺序,所以想要使用thread-loader优化某项的打包速度,必须放在其后执行
由于 thread-loader 引入后,需要 0.6s 左右的时间开启新的 node 进程,如果工程代码量小,引入 thread-loader 后,构建时间反而增加了0.19s,适得其反
因此,建议大型(超过150个页面)项目的优化时,仅在非常耗时的 loader 前引入 thread-loader即可。
5.2 happypack 已废弃
happypack 同样是用来设置多线程,但是在 webpack5官方也已经不再维护了,推荐使用 thread-loader。
6. 区分环境
Webpack5 模式(mode) 的不同模式的内置优化。
在开发过程中,切忌在开发环境使用生产环境才会用到的工具,如在开发环境下,应该排除 [fullhash]/[chunkhash]/[contenthash] 等工具。
同样,在生产环境,也应该避免使用开发环境才会用到的工具,如 webpack-dev-server 等插件。
7. 其他devtool
7.1 devtool的不同配置
不同的 devtool 设置,会导致性能差异。
在大多数情况下,最佳选择是 eval-cheap-module-source-map
详细区分可到官网 webpack devtool查看。其它参考站点
webpack.dev.js 配置方式如下:
arduino
export.module = {
devtool: 'eval-cheap-module-source-map',
}
7.2 输出结果不携带路径信息
默认 webpack 会在输出的 bundle 中生成路径信息,将路径信息删除可小幅提升构建速度。
ini
module.exports = {
output: {
pathinfo: false,
},
};
}
五、减小打包体积
1. 代码压缩
体积优化第一步是压缩代码,通过 webpack 插件,将 JS、CSS 等文件进行压缩。
1.1 JS 压缩
使用 TerserWebpackPlugin 来压缩 JavaScript。
webpack v5 开箱即带有最新版本的 terser-webpack-plugin。
如果你使用的是 webpack v5 或更高版本,同时希望自定义配置,那么仍需要安装 terser-webpack-plugin。
如果使用 webpack v4,则必须安装 terser-webpack-plugin v4 的版本。
terser-webpack-plugin 默认开启了 parallel: true 配置,并发运行的默认数量:os.cpus().length - 1
,本文配置的 parallel 数量为 4,使用多进程并发运行压缩以提高构建速度。
| 选项名 | 类型 | 默认值 | 描述 |
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------|------------------|--------------|------|--------------------|
| test | String | RegExp | Array<String | RegExp> | /.m?js(?.*)?$/i | 用来匹配需要压缩的文件。 |
| include | String | RegExp | Array<String | RegExp> | undefined | 匹配参与压缩的文件。 |
| exclude | String | RegExp | Array<String | RegExp> | undefined | 匹配不需要压缩的文件。 |
| parallel | Boolean | Number | true | 使用多进程并发运行以提高构建速度。 |
| minify | Function | TerserPlugin.terserMinify | 允许你自定义压缩函数。 |
| terserOptions | Object | default | Terser 的 minify 选项。 |
| extractComments | Boolean | String | RegExp | Function<(node, comment) -> Boolean | Object> | Object | true | 注释是否需要提取到一个单独的文件中。 |
1.1.2 ParallelUglifyPlugin 已废弃
ParallelUglifyPlugin 插件也可以帮助我们多进程压缩 JS,webpack5 的 TerserWebpackPlugin 默认就开启了多进程和缓存,无需再引入 ParallelUglifyPlugin。
1.2 CSS 压缩
使用 CssMinimizerWebpackPlugin 压缩 CSS 文件。和 optimize-css-assets-webpack-plugin 相比,
css-minimizer-webpack-plugin 在 source maps 和 assets 中使用查询字符串会更加准确,而且支持缓存和并发模式下运行。
CssMinimizerWebpackPlugin 将在 Webpack 构建期间搜索 CSS 文件,优化、压缩 CSS。
安装:
npm install -D css-minimizer-webpack-plugin
2. 代码分离
代码分离能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,可以缩短页面加载时间。
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。这会导致更大的初始下载量并减慢页面加载速度。
SplitChunksPlugin参数说明:
chunks: 指的的那些chunks需要进行优化,是一个字符串类型,有效值是:all,async和initial。
diff
- async这个值表示按需引入的模块将会被用于优化。
- initial表示项目中被直接引入的模块将会被用于优化。
- all顾名思义,表明直接引入和按需引入的模块都会被用于优化。
- minSize: 打包优化完生成的新chunk大小要> 30000字节,否则不生成新chunk。
- minChunks: 共享该module的最小chunk数
- maxAsyncRequests:最多有N个异步加载请求该module
- maxInitialRequests: 一个入口文件可以并行加载的最大文件数量
- automaticNameDelimiter:名字中间的间隔符
- name:chunk的名字,
如果设成true,会根据被提取的chunk自动生成。
值为 false 时,适合生产模式使用,webpack 会避免对 chunk 进行不必要的命名,以减小打包体积,
- 除了入口 chunk 外,其他 chunk 的名称都由 id 决定,
所以最终看到的打包结果是一排数字命名的 js,这也是为啥我们看线上网页请求的资源,总会掺杂一些 0.js,1.js 之类的文件(当然,使资源名为数字 id 的方式不止这一种,懒加载也能轻松办到)。
值为 string 时,缓存组最终会打包成一个 chunk,名称就是该 string。此外,当两个缓存组 name 一样,最终会打包在一个 chunk 中。你甚至可以把它设为一个入口的名称,从而将这个入口会移除。
cacheGroups: 这个就是重点了,我们要切割成的每一个新chunk就是一个cache group。
test:用来决定提取哪些module,可以接受字符串,正则表达式,或者函数,函数的一个参数为module,第二个参数为引用这个module的chunk(数组)。
priority:优先级高的chunk为被优先选择(说出来感觉好蠢),优先级一样的话,size大的优先被选择。
reuseExistingChunk: 当module未变时,是否可以使用之前的chunk。
要禁用任何默认缓存组,请将它们设置为false。例如 default:false
配置详解:
less
splitChunks: {
chunks: 'async', // 代码分割时对异步代码生效,all:所有代码有效,inital:同步代码有效
minSize: 30000, // 代码分割最小的模块大小,引入的模块大于 30000B 才做代码分割
maxSize: 0, // 代码分割最大的模块大小,大于这个值要进行代码分割,一般使用默认值
minChunks: 1, // 引入的次数大于等于1时才进行代码分割
maxAsyncRequests: 6, // 最大的异步请求数量,也就是同时加载的模块最大模块数量
maxInitialRequests: 4, // 入口文件做代码分割最多分成 4 个 js 文件
automaticNameDelimiter: '~', // 文件生成时的连接符
automaticNameMaxLength: 30, // 自动生成的文件名的最大长度
cacheGroups: {
vendors: {
test: /[\/]node_modules[\/]/, // 位于node_modules中的模块做代码分割
priority: -10 // 根据优先级决定打包到哪个组里,例如一个 node_modules 中的模块进行代码
}, // 分割,,既满足 vendors,又满足 default,那么根据优先级会打包到 vendors 组中。
default: { // 没有 test 表明所有的模块都能进入 default 组,但是注意它的优先级较低。
priority: -20, // 根据优先级决定打包到哪个组里,打包到优先级高的组里。
reuseExistingChunk: true // //如果一个模块已经被打包过了,那么再打包时就忽略这个上模块
}
}
}
壳工程中配置:
less
splitChunks: {
chunks: 'all',
cacheGroups:{
vendors:{ // node_modules里的代码
test: /[\/]node_modules[\/]/,
chunks: "all",
name: 'false', //不要定义固定的name
priority: 10, // 优先级
enforce: true
}
}
},
2.2 CSS 文件分离
MiniCssExtractPlugin
插件将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。
安装:npm install -D mini-css-extract-plugin
注意: MiniCssExtractPlugin.loader 要放在 style-loader 后面。 //不兼容webpack5,改用css-minimizer-webpack-plugin
插件
2.3 最小化 entry chunk
通过配置 optimization.runtimeChunk = true,为运行时代码创建一个额外的 chunk,减少 entry chunk 体积,提高性能。
webpack.prod.js 配置方式如下:
ini
module.exports = {
optimization: {
runtimeChunk: true,
},
};
}
3. Tree Shaking(摇树)
摇树,通俗讲代表项目中未引用的无用代码
3.1 JS
JS Tree Shaking
将 JavaScript 上下文中的未引用代码(Dead Code)移除,通过 package.json 的 "sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯正 ES2015 模块)",由此可以安全地删除文件中未使用的部分。
Dead Code 一般具有以下几个特征:
- 代码不会被执行,不可到达;
- 代码执行的结果不会被用到;
- 代码只会影响死变量(只写不读)。
3.1.1 webpack5 sideEffects
通过 package.json 的 "sideEffects" 属性,来实现这种方式。
json
{
"name": "your-project",
"sideEffects": false
}
注意的是,当代码有副作用时,需要将 sideEffects 改为提供一个数组,添加有副作用代码的文件路径:
json
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js"]
实际项目中配置为"sideEffects": false,未知副作用的包及js 始料未及
3.2 CSS
CSS 代码也需要摇摇树,打包时把没有用的 CSS 代码去除,可以大幅减少打包后的 CSS 文件大小。
使用 purgecss-webpack-plugin[50] 对 CSS Tree Shaking。
安装:npm i purgecss-webpack-plugin -D
因为打包时 CSS 默认放在 JS 文件内,因此要结合 webpack 分离 CSS 文件插件 mini-css-extract-plugin
一起使用,先将 CSS 文件分离,再进行 CSS Tree Shaking。
3.3 CDN
webpack 配置的优化,另一方面还可以通过 CDN 来减小打包体积。
这里引入 CDN 的首要目的为了减少打包体积,因此仅仅将一部分大的静态资源手动上传至 CDN,并修改本地引入路径。下文的加快加载速度,将介绍另一种 CDN 优化手段。
将大的静态资源上传至 CDN:
- 字体:压缩并上传至 CDN;
- 图片:压缩并上传至 CDN。
3.4 打包内容分析
webpack.prod.config.js配置:
javascript
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;//打包内容分析
new BundleAnalyzerPlugin({
analyzerMode: "server",
analyzerHost: "127.0.0.1",
analyzerPort: 8888, // 运行后的端口号
reportFilename: "report.html",
defaultSizes: "parsed",
openAnalyzer: true,
generateStatsFile: false,
statsFilename: "stats.json",
statsOptions: null,
logLevel: "info"
}),
效果分析:
根据报告结果分析,大文件可以再分包处理
六、加快加载速度
1. 按需加载
通过 webpack 提供的 import() 语法 ,动态导入 功能进行代码分离,通过按需加载,大大提升网页加载速度。
2. 浏览器缓存
加载的静态资源被浏览器缓存,再次进入网站后,将直接拉取缓存资源,加快加载速度。
webpack 支持根据资源内容,创建 hash id,当资源内容发生变化时,将会创建新的 hash id。
配置 JS bundle hash,webpack.common.js 配置方式如下:
java
module.exports = {
// 输出
output: {
// 仅在生产环境添加 hash
filename: isEnvProduction ? '[name]-v-[fullhash].js' : '[name].js', // 定义输出文件名
},
}
配置 CSS bundle hash,webpack.prod.js 配置方式如下:
java
module.exports = {
plugins: [
// 提取 CSS
new MiniCssExtractPlugin({
filename: "css/[name].[contenthash].css"
}),
],
}
配置 optimization.moduleIds,让公共包 splitChunks 的 hash 不因为新的依赖而改变,减少非必要的 hash 变动,webpack.prod.js 配置方式如下:
css
module.exports = {
optimization: {
moduleIds: 'deterministic',
}
}
通过配置 contenthash,浏览器缓存了未改动的文件,仅重新加载有改动的文件,加快加载速度。
3. CDN
将所有的静态资源,上传至 CDN,通过 CDN 加速来提升加载速度。
webpack.common.js 配置方式如下:
javascript
// 项目中尽可能使用客户提供的资源
export.modules = {
output: {
publicPath: isEnvProduction ? 'https://xxx.com' : '', // CDN 域名
},
}
七、优化前后对比
查看优化前后对比。
1. 构建速度run dev时
类型 | 首次运行 | 未修改内容二次运行 | 修改内容二次运行 |
---|---|---|---|
优化前 | 3342ms | 1706ms | 1589ms |
优化后 | 1.84s | 0.5s |
编译时报错:
打包速度 run build时
类型 | 首次构建 |
---|---|
优化前 | 1.04m |
优化后 | 59.90s |
2. 打包体积
类型 | 体积大小 |
---|---|
优化前 | 43.5MB |
优化后 | 13.1MB |
WebPack 警告WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).解决方案:壳工程使用方案一,关闭webpack自身性能瓶颈提示
javascript
// 方式一 webpack中添加如下配置
performance: {
hints:false
}
// 方式二 webpack中添加如下配置
performance: {
hints: "warning", // 枚举
maxAssetSize: 30000000, // 整数类型(以字节为单位)
maxEntrypointSize: 50000000, // 整数类型(以字节为单位)
assetFilter: function(assetFilename) {
// 提供资源文件名的断言函数
return assetFilename.endsWith('.css') || assetFilename.endsWith('.js');
}
},
八、小结
注:在小型项目中,添加过多的优化配置,作用不大,反而会因为额外的 loader、plugin 增加构建时间。
在加快构建时间方面,作用最大的是配置 cache,可大大加快二次构建速度。
在减小打包体积方面,作用最大的是压缩代码、分离重复代码、Tree Shaking,可最大幅度减小打包体积。
在加快加载速度方面,按需加载、浏览器缓存、CDN 提升效果显著。
九、依赖检查&依赖可视化
十、静态检查&提交规范
十一、webpack详细配置
webpack.common.js
javascript
/*
* @Author: lzx
* @Date: 2022-09-13 17:44:56
* @LastEditTime: 2022-10-08 10:22:03
* @LastEditors: DESKTOP-LADEI4B
* @Description: In User Settings Edit
* @FilePath: \wx20220903\wx-admin\webpack.common.js
*/
var fs = require('fs')
const lessToJs = require('less-vars-to-js')
const path = require('path')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CopyPlugin = require('copy-webpack-plugin')
const webpack = require('webpack')
const WebpackBar = require('webpackbar') // 进度条分析
// const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); //编译速度分析
const MomentLocalesPlugin = require('moment-locales-webpack-plugin') // moment按需加载;移除未用到的代码
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin') // lodash按需加载
const pkgJson = require('./package')
const configObj = pkgJson.config['' + process.env.NODE_ENV] || {}
const publicPath = configObj.publicPath
const CDN_BASE = configObj.CDN_BASE
//转换 less 变量,用于主题
const baseLess = lessToJs(
fs.readFileSync(path.join(__dirname, './src/themes/themes.less'), 'utf8')
)
let themers = {}
Object.keys(baseLess).map((k, i) => {
let key = String(k).replace(/@/g, '')
themers[key] = String(baseLess[k])
})
const lessVars = Object.assign(themers, {
'@CDN_BASE': JSON.stringify(CDN_BASE),
})
console.log('--------' + '当前构建环境是:' + process.env.NODE_ENV + '--------')
console.log('..........' + '开始构建' + '..........')
const isEnvProduction = process.env.NODE_ENV === 'production';
const isDev = process.env.NODE_ENV === 'development'
const commonConfig = {
entry: {
app: ['./src/index.js'], // 入口文件
},
output: {
filename: isEnvProduction ? '[name]-v-[fullhash].js' : '[name].js', // 定义输出文件名
// filename: '[name]-v-[fullhash].js',
path: path.resolve(__dirname, 'dist'), // 定义输出文件夹dist路径
publicPath: '',
// publicPath: isEnvProduction ? 'https://xxx.com' : 'publicPath', // CDN 域名
chunkFilename: '[name]-v-[fullhash].js',
pathinfo: false,
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './src/index.html'),
filename: 'index.html',
favicon: 'favicon.ico',
CDN_BASE: CDN_BASE,
publicPath: '',
inject: 'body', //js插入的位置,插入head中也会自动补defer="defer"属性以保证在页面加载后执行js,如果考虑兼容性可改成body
// scriptLoading: "defer",
minify: {
// 移除空格
collapseWhitespace: true,
// 移除注释
removeComments: true,
},
}),
new webpack.DefinePlugin({
process: {
NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'prod:dev'),
},
ENV: JSON.stringify(process.env.ENV || 'dev'),
LANGUAGES_ENV: JSON.stringify(process.env.LANGUAGES_ENV || 'zhCN'),
CDN_BASE: JSON.stringify(CDN_BASE || ''),
BACKEND_BASE: JSON.stringify(configObj.BACKEND_BASE),
publicPath: JSON.stringify(configObj.publicPath),
}),
new CopyPlugin({
patterns: [
{ from: path.resolve(__dirname, './src/assets'), to: 'assets' },
],
}),
new CleanWebpackPlugin(), // 清除之前的打包文件
new WebpackBar(),
new MomentLocalesPlugin(),
new LodashModuleReplacementPlugin(),
//全局引入jquery,此后在任何位置可直接使用$,Lodash或其他库也可以像这样引入,当然也可以在dist目录的lib文件夹下放第三方库,在html模板中直接引入
// new webpack.ProvidePlugin({
// '$':'jquery'
// })
],
cache: { // 减少二次构建时间
type: 'filesystem',
// 默认缓存到 node_modules/.cache/webpack 中
// 也可以自定义缓存目录
// cacheDirectory:path.resolve(__dirname,'node_modules/.cac/webpack')
},
resolve: {
alias: {
pages: path.join(__dirname, 'src/pages'),
widget: path.join(__dirname, 'src/widget'),
router: path.join(__dirname, 'src/router'),
utils: path.join(__dirname, 'src/utils'),
assets: path.join(__dirname, 'src/assets'),
themes: path.join(__dirname, 'src/themes'),
config: path.join(__dirname, 'src/config'),
'yx-widget': path.join(__dirname, 'src/yx-widget'),
},
modules: [path.join(__dirname, 'src'), 'node_modules'],
extensions: ['', '.js', '.jsx', '.less', '.css'], //配置文件后缀缩小查找范围
mainFields: ['loader', 'main'], // 从package.json文件中找入口文件。
mainFiles: ['index.js'],
symlinks: false, //如不使用npm link 可以设置 为 false,减少解析工作量。
},
resolveLoader: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
// 其中 __dirname 表示当前工作目录,也就是项目根目录
modules: [path.resolve(__dirname, 'node_modules')],
},
externals: {
// 'react': 'React',
// 'react-dom': 'ReactDOM',
// 'react-router-dom': 'ReactRouterDOM'
},
module: {
rules: [
{
test: /.css$/,
// use表示该文件类型需要调用的loader
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
},
},
// 'postcss-loader'
],
},
{
test: /.less$/,
exclude: /node_modules/,
use: [
'style-loader',
// isEnvProduction && MiniCssExtractPlugin.loader, // 仅生产环境
{
loader: 'css-loader',
options: {
importLoaders: 2,
modules: true,
},
},
// 'postcss-loader',
{
loader: 'less-loader',
options: {
lessOptions: {
relativeUrls: false,
modifyVars: lessVars,
javascriptEnabled: true,
},
},
},
],
},
{
test: /.(png|gif|bmp|jpg)$/,
type: 'asset/resource',
},
{
test: /.(woff(2)?|eot|ttf|otf|svg|)$/,
type: 'asset/inline',
},
{
test: /.(js|jsx)$/,
exclude: /node_modules/, //不解析
use: [
{
loader: 'babel-loader?cacheDirectory=true', // 开启babel-loader缓存',
options: {
presets: [
'@babel/preset-react',
'@babel/preset-env',
'react-app',
],
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }],
'@babel/plugin-syntax-jsx',
'@babel/plugin-proposal-object-rest-spread',
'transform-class-properties',
['import', { libraryName: 'antd', style: false }],
[
'babel-plugin-imports-transform',
{
'yx-widget': {
transform: 'yx-widget/component/${member}',
preventFullImport: true,
style: 'yx-widget/component/${member}/index.less',
},
'yx-widgetDemo': {
transform: 'yx-widget/demo/${member}',
preventFullImport: true,
style: 'yx-widget/demo/${member}/index.less',
},
},
],
isDev && require.resolve('react-refresh/babel'),
].filter(Boolean),
},
},
],
},
],
},
}
module.exports = {
lessVars,
commonConfig,
}
webpack.dev.config.js
php
/*
* @Author: your name
* @Date: 2022-09-13 17:44:56
* @LastEditTime: 2022-10-08 10:22:10
* @LastEditors: DESKTOP-LADEI4B
* @Description: In User Settings Edit
* @FilePath: \wx20220903\wx-admin\webpack.dev.config.js
*/
const { merge } = require('webpack-merge')
const path = require('path')
const { lessVars, commonConfig } = require('./webpack.common.js')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
const webpack = require('webpack')
const devConfig = merge(commonConfig, {
mode: 'development',
devtool: "eval-cheap-module-source-map", //控制台提示信息映射
target: 'web',
// 使用webpck-dev-server时配置
devServer: {
contentBase: path.resolve(__dirname, "dist"),
port: "4098",
proxy: process.env.ENV === 'loc' ? { //反向代理,根据需求自行修改
"/viewshine": {
target: "xxxx",
changeOrigin: true,
// pathRewrite: {
// "^/viewshine": ""
// }
}
}:{},
open: true,
host: 'localhost',
hotOnly: false, // 页面构建失败不刷新页面
hot: true, //让webpackDevServer开启热更新功能
inline:true,
// overlay: { warnings: true, errors: true },
// compress: true,
// disableHostCheck: true,
// historyApiFallback: true,
},
//如需热更新,开启下面代码
plugins: [
// new webpack.HotModuleReplacementPlugin(),
new ReactRefreshWebpackPlugin({
overlay:false //该属性是用来在编译出错的时候,在浏览器页面上显示错误。
}),
]
});
module.exports =devConfig
webpack.prod.config.js
php
/*
* @Author: your name
* @Date: 2022-09-13 17:44:56
* @LastEditTime: 2023-09-20 13:11:06
* @LastEditors: lzxaily1107 493535562@qq.com
* @Description: In User Settings Edit
* @FilePath: \wx20220903\wx-admin\webpack.prod.config.js
*/
const Path = require("path");
const glob = require('glob');
const { merge } = require('webpack-merge'); //引入配置文件合并工具
// const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const TerserJSPlugin = require("terser-webpack-plugin"); //js压缩
const {commonConfig,less} = require('./webpack.common.js'); //引入公共配置
const MiniCssExtractPlugin = require('mini-css-extract-plugin');// 不兼容weback5
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const PurgecssPlugin = require('purgecss-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;//打包内容分析
const PATHS = {
src: Path.join(__dirname, 'src')
};
const RE_VENDOR =/[\/]node_modules[\/](react|react-dom|mobx|mobx-react|mobx-react-router|react-router|react-router-dom|moment|moment/locale/zh-cn|jquery|lodash|axios|js-cookie)[\/]/;
process.env.NODE_ENV === "production";
const prodConfig = merge(commonConfig, {
mode: "production",
optimization: {
runtimeChunk: true,
moduleIds: 'deterministic', //减少非必要的hash变动
minimize: true,
minimizer: [
//js压缩混稀
new TerserJSPlugin({
test: /.js(?.*)?$/i, //匹配参与压缩的文件
parallel: 4, //使用多进程并发运行
terserOptions: { //Terser 压缩配置
output:{comments: false}
},
extractComments: true, //将注释剥离到单独的文件中
}),
// new OptimizeCSSAssetsPlugin({}), //css压缩混稀
//css压缩
new CssMinimizerPlugin({
parallel: 4,
}),
],
splitChunks: {
chunks: 'all',
cacheGroups:{
vendors:{ // node_modules里的代码
test: /[\/]node_modules[\/]/,
// test: /[\/]node_modules[\/](react|react-dom)[\/]/,
// test:RE_VENDOR,
chunks: "all",
name: 'vendors', //一定不要定义固定的name
priority: 10, // 优先级
enforce: true
}
}
},
usedExports: true, //开启优化(树摇但保留代码)
minimize: true //开启压缩 (删除未使用代码)
},
devtool: 'source-map',
plugins: [
new CleanWebpackPlugin(),
// 提取 CSS
new MiniCssExtractPlugin({
filename: "css/[name].[contenthash].css"
}),
// 开启css的tree-shaking
new PurgecssPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
}),
new BundleAnalyzerPlugin({
analyzerMode: "server",
analyzerHost: "127.0.0.1",
analyzerPort: 8888, // 运行后的端口号
reportFilename: "report.html",
defaultSizes: "parsed",
openAnalyzer: true,
generateStatsFile: false,
statsFilename: "stats.json",
statsOptions: null,
logLevel: "info"
}),
],
//关闭 webpack 的性能提示
performance: {
hints:false
}
});
module.exports =prodConfig
注: 某些原因,约束本地升级包管理仅能使用npm
十二、项目地址
codeUp:codeup.aliyun.com/6322fc75d4d...[
](blog.csdn.net/pcaxb/artic...)
视频讲解可前往:www.yuque.com/donsoft/qkn...