介绍
此文章基于webpack5来阐述
webpack性能优化较多,可以对其进行分类
- 优化打包速度,开发或者构建时优化打包速度(比如exclude、catch等)
- 优化打包后的结果,上线时的优化(比如分包处理、减小包体积、CDN服务等)
大多数情况下,我们会更侧重于优化打包后的结果,这对于线上产品的影响更大。
优化构建速度
对于 优化构建速度 会从 定向查找
、减少执行构建的模块
、合理使用缓存
、并行构建以提升总体速度
、并行压缩提高构建效率
几个方面入手。
定向查找
resolve.modules
webpack
的 resolve.modules
配置用于指定 webpack
去哪些目录下寻找第三方模块。其默认值是 ['node_modules']
,webpack
在寻找的时候,会先去当前目录的 ./node_modules
下去查找,没有找到就会再去上一级目录 ../node_modules
中去找,直到找到为止。
所以如果我们项目的第三方依赖模块放置的位置没有变更的话,可以使用绝对路径减少查找的时间,配置如下:
module.export = {
resolve: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
// __dirname 表示当前工作目录,也就是项目根目录
modules: [path.resolve(__dirname, 'node_modules')]
}
}
resolve.extensions
extensions
是我们常用的一个配置,适用于指定在导入语句没有带文件后缀时,可以按照配置的列表,自动补上后缀。我们应该根据我们项目中文件的实际使用情况设置后缀列表,将使用频率高的放在前面、同时后缀列表也要尽可能的少,减少没有必要的匹配。同时,我们在源码中写导入语句的时候,尽量带上后缀,避免查找匹配浪费时间。
同时,我们在源码中写导入语句的时候,尽量带上后缀,避免查找过程。
module.export = {
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
}
}
extensions
的默认值是['.js', '.json', '.wasm']
,所以不写文件后缀的话,它会依次匹配'.js', '.json', '.wasm'
,如果都没匹配上的话才会报错。
减少执行构建的模块
合理配置 loader 的 include、exclude
loader
对文件的转换是个耗时的操作,并且 loader
的配置会批量命中多个文件,所以需要根据自己的项目尽可能的精准命中哪些文件是需要被 loader 处理的。
webpack 提供了 test、include、exclude
三个配置项来命中 loader 。
比如,我们只想对根目录 src 下的 js 文件使用 babel-loader
进行处理可以这样设置:
const path = require('path');
module.exports = {
//...
module: {
rules: [
{
test: /.jsx?$/,
use: ['babel-loader'],
include: [path.resolve(__dirname, 'src')]
}
]
},
}
或者我们想排除node_modules
目录下的js
使用 babel-loader
进行处理可以这样设置:
const path = require('path');
module.exports = {
//...
module: {
rules: [
{
test: /.jsx?$/,
use: ['babel-loader'],
exclude: /node_modules/, //排除 node_modules 目录
}
]
},
}
合理使用缓存
在优化的方案中,缓存也是其中重要的一环。在构建过程中,可以通过使用缓存提升二次打包速度。
babel-loader 开启缓存
cacheDirectory
的默认值为 false
。当有设置时,指定的目录将用来缓存 loader
的执行结果。之后的 webpack
构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的 Babel
重新编译过程。
module.exports = {
module: {
rules: [
{
test: /.jsx?$/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
}
]
}
]
}
}
cache-loader
没有缓存配置的loader该怎么使用缓存呢?那就得借助 cache-loader
。
其使用如下:
module.exports = {
module: {
rules: [
{
test: /.jsx?$/,
use: [
'cache-loader', //需要安装
"babel-loader"
],
}
]
}
}
webpack5 配置 cache.type
webpack5
新增的cache
属性,可以开启磁盘缓存,默认将编译结果缓存在 node_modules/.cache/webpack
目录下。
cache
会在开发
模式被设置成 type: 'memory'
而且在 生产
模式中被禁用。 cache: true
与 cache: { type: 'memory' }
配置作用一致。 传入 false
会禁用缓存。
经过配置,再次构建可以看到.cache/webpack
下生成了缓存文件
当然缓存也是不能盲目使用,也是需要斟酌,因为保存和读取这些缓存文件也会有一些时间开销,所以建议只对性能开销较大的 loader
采用改缓存优化。
并行构建以提升总体速度
默认情况下,webpack 是单线程模型,一次只能处理一个任务,在文件过多时会导致构建速度变慢。所以在减少了需要执行构建的模块和降低了单个模块的构建速度之外,我们还可以并行构建,让 webpack 同时处理多个任务,发挥多核 CPU 的优势。
Thread-loader
Thread-loader
,会创建多个 worker
池进行并发执行构建任务,但是使用起来更为简单。只要将这个 loader
放置在其他 loader
之前, 放置在这个 Thread-loader
之后的 loader 就会在一个单独的 worker 池(worker pool) 中运行。
其使用如下:
module.exports = {
module: {
rules: [
{
test: /.jsx?$/,
use: [
// 开启多进程打包。
{
loader: 'thread-loader', // 需要安装
options: {
workers: 3 // 进程3个
}
},
{
loader: 'babel-loader',
}
]
}
]
}
}
在 webpack 官网 中也有提示,每个 worker 都是一个单独的有 600ms 限制的 node.js
进程。同时跨进程的数据交换也会被限制。所以建议仅在耗时的 loader 上使用。
只有在代码量很多的时候开启多进程构建才会有明显的提升,如果项目很简单,代码量少可能会适得其反。所以使用前需要斟酌,不要为了优化而优化。
并行压缩提高构建效率
前面说了并行构建,下面来说说并行压缩。
TerserWebpackPlugin 开启 paralle
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({ parallel: true }), // 默认已经开启,其实无需设置
],
},
};
优化构建结果
对于 优化构建结果 我们可以从 压缩代码
、按需加载
、预加载
、Code Splitting
、Tree Shaking
、Gzip
、作用提升
几个方面入手。
压缩代码
压缩 html
压缩 html
使用的还是 html-webpack-plugin
插件。该插件支持配置一个 minify 对象,用来配置压缩 html
。
module.export = {
plugins: [
new HtmlWebpackPlugin({
// 动态生成 html 文件
template: "./index.html",
minify: {
// 压缩HTML
removeComments: true, // 移除HTML中的注释
collapseWhitespace: true, // 删除空⽩符与换⾏符
minifyCSS: true // 压缩内联css
},
})
]
}
压缩 css
在webpack5
中推荐使用的是 css-minimizer-webpack-plugin。
首先我们在入口文件里面引入test.css
文件
import "./test.css";
test.css
文件内容如下
.box {
background-color: red;
}
.item {
color: black;
}
配置方式:
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
optimization: {
// 是否需要压缩
minimize: true, // 需要开启
// 配置压缩工具
minimizer: [
// 添加 css 压缩配置
new CssMinimizerPlugin({}), // 需要安装
],
},
重新构建,会单独生成了 main.css
文件,并对css
进行了压缩。
压缩 js
目前的主流还是terser-webpack-plugin
,在webpack5
生产环境中(mode=production
),已默认开启。
如果你不想使用terser-webpack-plugin
插件的默认配置,想自定义,也是支持的,可以直接引入terser-webpack-plugin
进行自定义配置即可。
const TerserPlugin = require("terser-webpack-plugin"); // webpack5内置,不需要再单独安装
optimization: {
// 是否需要压缩
minimize: true,
// 配置压缩工具
minimizer: [
new TerserPlugin({// 在这里自定义配置}),
],
},
按需加载
很多时候不需要一次性加载所有的JS
文件,而应该在不同阶段去加载所需要的代码。webpack
内置了强大的分割代码的功能可以实现按需加载。
比如,我们在点击了某个按钮之后,才需要使用使用对应的JS
文件中的代码,我们可以使用 import()
语法按需引入
// index.js
document.getElementById('btn1').onclick = function() {
import('./impModule.js').then(fn => fn.default());
}
impModule.js
export default () => {
console.log("我是懒加载模块");
};
我们打包之后,动态加载的模块会单独生成一个js
文件。
页面首次加载的时候并不会加载该js
文件,而是当我们需要使用到的时候才会进行加载。我们在页面上看看效果。
在默认情况下打包出来的文件名是路径的组合,比如上面的src_impModule.js
,如果你不想使用这个名字,想通俗易懂可以在import
里面配置 webpackChunkName
魔法注释。
比如上面的例子,我们配置ebpackChunkName: "btnChunk"
// index.js
document.getElementById('btn1').onclick = function() {
import(/* webpackChunkName: "btnChunk" */ './impModule.js').then(fn => fn.default());
}
再次构建可以看到,构建出来的文件名就是我们事先定义好的名称
预加载(prefetch 和 preload)
上面说的代码懒加载在使用的时候才去加载是会提升页面性能,但是如果懒加载的模块比较大,当点击的时候再去加载的话无疑会让用户等待时间加长。
可以利用浏览器空闲时候去加载这些切分出来的模块,那就是prefetch 和 preload
prefetch和preload的概念
prefetch
(预取):将来可能需要一些模块资源,在核心代码加载完成之后浏览器空闲的时候再去加载需要用到的模块代码。
preload
(预加载):当前核心代码加载期间可能需要模块资源,其是和核心代码文件一起去加载的。
prefetch
我们将上面的例子稍微改下,加个注释/* webpackPrefetch: true */
// index.js
document.getElementById("btn1").onclick = async () => {
const imp = await import(/* webpackPrefetch: true */ "./impModule.js");
imp.default();
};
上面的代码的意思是当我们主要的核心代码加载完成,浏览器有空闲的时候,浏览器就会帮我们自动的去下载impModule.js
在head
里面,懒加载模块会被直接引入了,并且会添加rel='prefetch'
。
preload
prefetch 与 preload 的区别
preload chunk
会在父chunk
加载时,以并行方式开始加载。prefetch chunk
会在父chunk
加载结束后开始加载。preload chunk
具有中等优先级,并立即下载。prefetch chunk
在浏览器闲置时下载。preload chunk
会在父chunk
中立即请求,用于当下时刻。prefetch chunk
会用于未来的某个时刻。- 浏览器支持程度不同,需要注意。
Code Splitting (代码分割)
要理解webpack的提出的几个概念,module、chunk和bundle。
module:每个import引入的文件就是一个模块
chunk:当module源文件传到webpack进行打包时,webpack会根据文件引用关系生成chunk
bundle:是对chunk进行压缩、分割等处理后的产物
SplitChunksPlugin
可以阅读我的另外一篇文章,里面讲述了一些拆包策略 SplitChunksPlugin
MiniCssExtractPlugin
可以利用 mini-css-extract-plugin 插件,将我们的css
代码分离出来。
接下来我们实操下。
创建样式文件
// index.less
.a {
background-color: aqua;
}
.b {
font-size: 18px;
}
配置mini-css-extract-plugin
插件
//webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
// ...
{
test: /.less?$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"],
exclude: /node_modules/, //排除 node_modules 目录
},
plugins: [
// ...
new MiniCssExtractPlugin(),
}),
],
css
代码的分离优化原理其实和js
是一样的。第一就是拆出来利用浏览器并发请求特性进行快速加载,其次就是多页面如果用到了相同样式能进行复用。
注意,此插件为每个包含 css
的 js
文件创建一个单独的 css
文件。
这句话的意思就是,css
代码的分割不是以引入了多少个css
文件构建后就有多少个css
文件,而是根据你的js
包来的,如果构建后你的js
包只有一个那么css
包也只会有一个,而不管你源代码里引入了多少个css
文件。
Tree Shaking
利用 ES Module 静态分析的特点来检测模块内容的导出、导入以及被使用的情况,保留被使用的代码,消除不会被执行和没有副作用(Side Effect) 的 死代码。
Tree Shaking 最先出现在rollup中,rollup 是在编译打包过程中分析程序流,得益于 ES6 静态模块(exports 和 imports 不能在运行时修改),我们在打包时就可以确定哪些代码是我们需要的。
webpack 本身在打包时只能标记未使用的代码而不移除,而识别代码未使用标记并完成 tree-shaking 的 其实是 UglifyJS、terser 这类压缩代码的工具。简单来说,就是压缩工具读取 webpack 打包结果,在压缩之前移除 bundle 中未使用的代码
webpack 中的 sideEffects 表示是否要开启识别 package.json 中 sideEffects 标记的功能,而 package.json 中的 sideEffects 是为了告知打包工具该模块是否包含副作用或者包含哪些副作用。
Gzip
前端除了在打包的时候将无用的代码或者 console
、注释剔除之外。还可以使用 Gzip
对资源进行进一步压缩。
- 当用户访问 web 站点的时候,会在
request header
中设置accept-encoding: gzip
,表明浏览器是否支持Gzip
。 - 服务器在收到请求后,判断如果需要返回
Gzip
压缩后的文件那么服务器就会先将我们的JS\CSS
等其他资源文件进行Gzip
压缩后再传输到客户端,同时将response headers
设置content-encoding:gzip
。反之,则返回源文件。 - 浏览器在接收到服务器返回的文件后,判断服务端返回的内容是否为压缩过的内容,是的话则进行解压操作。
const CompressionWebpackPlugin = require("compression-webpack-plugin"); // 需要安装
module.exports = {
plugins: [
new CompressionWebpackPlugin()
]
}
配置好我们再来构建,会生成资源的.gz
文件
当然在Nginx上也可以在配置文件中启用 gzip 压缩
作用提升 (Scope Hoisting)
Scope Hoisting
作用域提升,在 JavaScript
中,也有类似的概念,“变量提升”、“函数提升”,JavaScript
会把函数和变量声明提升到当前作用域的顶部,Scope Hoisting
也是类似。webpack
会把引入的 js 文件“提升”顶部。
在没有使用 Scope Hoisting
的时候,webpack
的打包文件会将各个模块分开使用 __webpack_require__
导入,在使用了 Scope Hoisting
之后,就会把需要导入的文件直接移入使用模块的顶部。这样做的好处有
- 代码中函数声明和引用语句减少,减少代码体积
- 不用多次使用
__webpack_require__
调用模块,运行速度会的得以提升。
所以,Scope Hoisting
可以让 webpack
打包出来的代码文件体积更小,运行更快。
因为 Scope Hoisting
需要分析模块之间的依赖关系,所以源码必须采用 ES6 模块化语法。也就是说如果你使用非 ES6
模块或者使用 import()
动态导入的话,则不会有 Scope Hoisting
。
Scope Hoisting
是 webpack
内置功能,只需要在plugins
里面使用即可
module.exports = {
plugins: [
// 开启 Scope Hoisting 功能
new webpack.optimize.ModuleConcatenationPlugin()
]
}
不过生产环境下 Scope Hoisting 功能是默认开启的,不用再额外处理。
常用分析工具
时间分析工具 speed-measure-webpack-plugin
speed-measure-webpack-plugin 这个插件分析整个打包的总耗时,以及每一个loader 和每一个 plugins 构建所耗费的时间,从而帮助我们快速定位到可以优化 Webpack 的配置。
使用如下
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); // 需要安装
const smp = new SpeedMeasurePlugin();
module.exports = () => smp.wrap(config); // 使用smp包裹webpack的配置
构建结果分析工具 webpack-bundle-analyze
webpack-bundle-analyzer
能可视化的反映
- 打包出的文件中都包含了什么;
- 每个文件的尺寸在总体中的占比,哪些文件尺寸大;
- 模块之间的包含关系;
- 是否有重复的依赖项,是否存在一个库在多个文件中重复? 或者包中是否具有同一库的多个版本?
- 每个文件的压缩后的大小。
使用如下:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; // 需要安装
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}