webpack进阶之性能优化(webpack5最新版本)
简介
说到webpack的性能优化,其实就是从时间层面与体积层面入手。对于时间层面主要就是优化webpack的构建速度(缩短构建时间)。对于体积层面主要就是优化webpack的构建结果(缩小构建结果)。
本文使用的 webpack 版本为 5.74.0、webpack-cli 的版本为 4.10.0
优化构建速度
对于 优化构建速度 我们可以从 定向查找、减少执行构建的模块、并行构建以提升总体速度、并行压缩提高构建效率、合理使用缓存几个方面入手。

定向查找
webpack 的 resolve 配置了模块会按照什么规则如何被解析,webpack 提供合理的默认值,但是还是可能会修改一些解析的细节。我们来看下如何修改 resolve 配置加快构建速度。
resolve.modules
webpack 的 resolve.modules 配置用于指定 webpack 去哪些目录下寻找第三方模块。其默认值是 ['node_modules'],webpack 在寻找的时候,会先去当前目录的 ./node_modules 下去查找,没有找到就会再去上一级目录 ../node_modules 中去找,直到找到为止。
所以如果我们项目的第三方依赖模块放置的位置没有变更的话,可以使用绝对路径减少查找的时间,配置如下:
module.export = {
resolve: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
// __diename 表示当前工作目录,也就是项目根目录
modules: [path.resolve(__dirname, 'node_modules')]
}
}
resolve.extensions
extensions 是我们常用的一个配置,适用于指定在导入语句没有带文件后缀时,可以按照配置的列表,自动补上后缀。我们应该根据我们项目中文件的实际使用情况设置后缀列表,将使用频率高的放在前面、同时后缀列表也要尽可能的少,减少没有必要的匹配。同时,我们在源码中写导入语句的时候,尽量带上后缀,避免查找匹配浪费时间。
同时,我们在源码中写导入语句的时候,尽量带上后缀,避免查找过程。
module.export = {
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
}
}
extensions的默认值是['.js', '.json', '.wasm'],所以不写文件后缀的话,它会依次匹配'.js', '.json', '.wasm',如果都没匹配上的话才会报错。
还需要注意的是如果配置了extensions会替代默认值,如果还需要使用默认值,需要配置...。
module.export = {
resolve: {
// 相当于 '.js', '.json', '.wasm', '.ts', '.tsx'
extensions: ["...", '.ts', '.tsx'],
}
}
减少执行构建的模块
我们都知道,webpack 的构建过程是从 entry 出发,然后依次递归解析出文件的导入语句。在遇到导入语句的时候,要判断是否需要使用设置的 loader 去处理文件。因为这里涉及到了递归操作,所以在文件较少的时候性能问题可能不明显,在我们的项目逐步壮大有很多文件之后,依赖关系就会变得复杂,这个时候递归的速度的问题就会慢慢暴露出来了。所以我们先要缩小文件的搜索范围。
缩小文件的搜索范围主要从以下几个方面入手
合理配置 noParse
noParse 配置的意思是让 webpack 忽略没有模块化的文件,比如 JQuery、lodash。而这些三方库里面没有其他依赖,可以通过配置noParse不去解析这些文件,提高打包效率。
需要注意的是,被忽略掉的文件中如果包含 import、require、define 等模块化语句时,在构建产物中也会包含,浏览器无法识别的时候就会报错。所以配置noParse的时候一定要清楚这个模块里面是否使用了import、require、export 等模块化语句。
比如笔者入口文件使用了 lodash和jquery。
import _ from "lodash";
import $ from "jquery";
console.log(_.join(["a", "b", "c"], "-"));
console.log($);
在没配置noParse的时候构建时间为 3778ms

当配置noParse后
module.export = {
module: {
noParse: /jquery|lodash/,
rules: [
{
test: /\.jsx?$/,
use: ["babel-loader"],
}
],
},
}
构建时间为 3052ms

合理配置 IgnorePlugin
有的依赖包,除了项目所需要的模块外,还会附带一些多余的模块。典型的例子就是 moment 这个包,一般情况下在构建时会自动引入其 local 目录下的多国语言包。
但是对于大多数情况而言,项目中只需要引入本国语言包即可,而 webpack 提供的 IgnorePlugin 即可在构建模块时直接删除那些需要被排除的模块,从而提升模块的构建速度,并减少产物体积。
比如笔者入口文件使用了 moment。
import moment from "moment";
// 设置中文
moment.locale("zh-cn");
let time = moment().endOf("day").fromNow();
console.log(time);
在没配置 IgnorePlugin的时候构建结果体积和时间分别为1.65mb、1082ms

当配置IgnorePlugin后
module.export = {
plugins: [
new webpack.IgnorePlugin(/^./locale$/, /moment$/)
]
}
构建结果体积和时间分别为1.1mb、831ms

可以发现配置IgnorePlugin不仅可以优化构建速度还可以优化构建结果。
合理配置 externals
externals 会告诉 Webpack 无需打包哪些库文件。
比如笔者入口文件使用了 jquery。
import $ from "jquery";
console.log($);
在没配置 externals的时候构建结果体积和时间分别为320kb、622ms

当配置externals后
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
将 jQuery 配置在 externals 中,告诉 webpack 将 JQuery 模块从构建过程中移除。
module.exports = {
//...
externals: {
jquery: '$',
},
};
构建结果体积和时间分别为5.36kb、616ms

可以发现配置externals不仅可以优化构建速度还可以优化构建结果。
上面我们的cdn是手动引入的,如果使用的包很多的话一个个手动加就很麻烦了,有没有办法能自动添加呢?
那就得借助 html-webpack-externals-plugin 插件动态配置cdn了。
我们来改造下,将之前在index.html里面的cdn删除掉,并将webpack.config.js里面配置的externals删除,然后直接使用html-webpack-externals-plugin插件。
// webpack.config.js
const HtmlWebpackExternalsPlugin = require("html-webpack-externals-plugin");
plugins: [
new HtmlWebpackExternalsPlugin({
externals: [
{
module: "jquery",
entry: "http://libs.baidu.com/jquery/2.0.0/jquery.min.js",
global: "$",
},
],
}),
]
再次构建,可以发现cdn被自动引入到了index.html

这样后续有新的cdn我们就可以直接在HtmlWebpackExternalsPlugin里面配置就可以啦。
正所谓有利就有弊,使用cdn的好处就是能减小构建后包的体积,但是依赖网络,依赖cdn的稳定性,所以这方面也需要权衡一下。
合理配置 loader 的 include、exclude
loader 对文件的转换是个耗时的操作,并且 loader 的配置会批量命中多个文件,所以我们需要根据自己的项目尽可能的精准命中哪些文件是需要被 loader 处理的。
webpack 提供了 test、include、exclude 三个配置项来命中 loader 。
include 的意思是只对命中的模块使用特定的 loader 进行处理,exclude 的意思是指定排除的文件,不使用该 loader 进行处理。
比如,我们只想对根目录 src 下的 js 文件使用 babel-loader 进行处理可以这样设置:
//webpack.config.js
const path = require('path');
module.exports = {
//...
module: {
rules: [
{
test: /.jsx?$/,
use: ['babel-loader'],
include: [path.resolve(__dirname, 'src')]
}
]
},
}
或者我们想排除node_modules目录下的js使用 babel-loader 进行处理可以这样设置:
//webpack.config.js
const path = require('path');
module.exports = {
//...
module: {
rules: [
{
test: /.jsx?$/,
use: ['babel-loader'],
exclude: /node_modules/, //排除 node_modules 目录
}
]
},
}
比如笔者入口文件使用了 jquery。
import $ from "jquery";
console.log($);
在没有配置exclude: /node_modules/ 构建时间为1647ms

配置了exclude: /node_modules/ 构建时间为628ms,时间缩短了一倍,提升还是相当大的。

并行构建以提升总体速度
默认情况下,webpack 是单线程模型,一次只能处理一个任务,在文件过多时会导致构建速度变慢。所以在减少了需要执行构建的模块和降低了单个模块的构建速度之外,我们还可以并行构建,让 webpack 同时处理多个任务,发挥多核 CPU 的优势。
HappyPack
HappyPack 是一个老牌的 webpack 并行处理任务的插件。其可以在 loader 的执行过程由单进程扩展为多进程模式。将任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。从而加速代码构建(但是仅限于对 loader 的处理)。
HappyPack 会自动进行分解和管理任务,我们在使用的时候只需要接入 HappyPack 插件即可。我们需要将通过 Loader 处理的文件先交给 happyPack/loader 去处理。每实例化一个 HappyPack,就是告诉 HappyPack 创建一个进程池来管理生成的子进程对象。其使用姿势如下:
const HappyPack = require('happypack') // 需要安装
const happyThreadPool = HappyPack.ThreadPool({size: 3})
module.exports = {
module: {
rules: [
{
test: /.jsx?$/,
// 用 HappyPack 的 loader 替换当前 loaders:
loader: 'happypack/loader?id=happyBabel',
}
]
},
plugins: [
new HappyPack({
// id 标识 happypack 处理那一类文件
id: 'happyBabel',
// 配置loader
loaders: [{
loader: 'babel-loader?cacheDirectory=true'
}],
// 共享进程池
threadPool: happyThreadPool,
// 日志输出
verbose: true
})
]
}
为了充分发挥多核的优势,笔者在入口文件使用了jquery、lodash、moment
import _ from "lodash";
import $ from "jquery";
import moment from "moment";
console.log(_.join(["a", "b", "c"], "-"));
console.log($);
// 设置中文
moment.locale("zh-cn");
let time = moment().endOf("day").fromNow();
console.log(time);
document.getElementById("root").innerHTML = time;
在没配置HappyPack的情况下构建时间为5009ms

配置了HappyPack的情况下构建时间为3325ms,有了一定的提升

Thread-loader
Thread-loader 和 HappyPack 类似,会创建多个 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',
}
]
}
]
}
}
上面的例子,我们改成使用thread-loader构建时间为3205ms。

在 webpack 官网 中也有提示,每个 worker 都是一个单独的有 600ms 限制的 node.js 进程。同时跨进程的数据交换也会被限制。所以建议仅在耗时的 loader 上使用。
只有在代码量很多的时候开启多进程构建才会有明显的提升,如果项目很简单,代码量少可能会适得其反。所以使用前需要斟酌,不要为了优化而优化。
并行压缩提高构建效率
前面说了并行构建,下面来说说并行压缩。
在看并行压缩前,笔者建议你先看看文章后面写的优化构建结果里面的压缩js代码片段。了解有哪些压缩js的方式后再来看并行压缩可能会理解的顺畅点。
压缩js主要使用 uglifyjs-webpack-plugin 和 terser-webpack-plugin 插件。这两个插件都可以设置 parallel 参数启用多进程并行来提高构建速度。
UglifyjsWebpackPlugin、TerserWebpackPlugin 开启 paralle
// webpack.config.js
module.exports = {
optimization: {
minimizer: [
new UglifyJsPlugin({parallel: true}), // 开启多进程
// new TerserPlugin({ parallel: true }), // 默认已经开启,其实无需设置
],
},
};
ParallelUglifyPlugin (已过时)
ParallelUglifyPlugin 和 HappyPack 类似,都是通过并行处理任务的方式提升构建速度。
但是 ParallelUglifyPlugin 是作用在代码压缩阶段。和处理 Loader 一样,webpack 在使用 UglifyJS 进行压缩的时候也是只能一个一个进行处理。ParallelUglifyPlugin 所要做的就是开启多个子进程并行处理任务,将任务分配给多个子进程完成,每个子进程分别使用 UglifyJS 进行压缩。
这个插件有三四年没更新了,也基本不会用到,我们了解即可。目前压缩js的主流还是terser-webpack-plugin。
合理使用缓存
在优化的方案中,缓存也是其中重要的一环。在构建过程中,我们可以通过使用缓存提升二次打包速度。主要有以下几种方式:
babel-loader 开启缓存
我们知道,前端代码里面js文件占大头,所以编译js的babel就默认支持了缓存的配置。我们可以使用 cacheDirectory参数开启缓存。
cacheDirectory 的默认值为 false。当有设置时,指定的目录将用来缓存 loader 的执行结果。之后的 webpack 构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的 Babel 重新编译过程(recompilation process)。
如果设置了一个空值 (loader: 'babel-loader?cacheDirectory') 或者 true (loader: 'babel-loader?cacheDirectory=true'),loader将使用默认的缓存目录 node_modules/.cache/babel-loader,如果在任何根目录下都没有找到 node_modules 目录,将会降级回退到操作系统默认的临时文件目录。
比如笔者的入口文件使用了如下代码
import _ from "lodash";
import $ from "jquery";
import moment from "moment";
console.log(_.join(["a", "b", "c"], "-"));
console.log($);
// 设置中文
moment.locale("zh-cn");
let time = moment().endOf("day").fromNow();
console.log(time);
document.getElementById("root").innerHTML = time;
配置好bable-loader的缓存cacheDirectory
module.exports = {
module: {
rules: [
{
test: /.jsx?$/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
}
]
}
]
}
}
在第一构建的时候花费的时间为4995ms

构建完后在.cache目录下产生了缓存文件。

第二次构建的时候花费的时间为1722ms,大大缩短了构建时间

cache-loader
没有缓存配置的loader该怎么使用缓存呢?那就得借助 cache-loader啦。
其使用姿势如下:
module.exports = {
module: {
rules: [
{
test: /.jsx?$/,
use: [
'cache-loader', //需要安装
"babel-loader"
],
}
]
}
}
还是上面的例子,配置好cache-loader后,第一次构建时间为5287ms

构建完后在.cache目录下产生了缓存文件。

第二次构建的时候花费的时间为1041ms,大大缩短了构建时间。

HardSourceWebpackPlugin (已过时)
HardSourceWebpackPlugin 的作用是为模块提供中间缓存,缓存默认的存放在是 node_modules/.cache/hard-source 中。
因为 HardSourceWebpackPlugin 四五年没更新了,并且在webpack5中会报错(已废弃不支持)。这里笔者就不再举例了,了解即可。
webpack5 配置 cache.type
webpack5新增的cache属性,可以开启磁盘缓存,默认将编译结果缓存在 node_modules/.cache/webpack目录下。
cache 会在开发 模式被设置成 type: 'memory' 而且在 生产 模式中被禁用。 cache: true 与 cache: { type: 'memory' } 配置作用一致。 传入 false 会禁用缓存。当将 cache.type 设置为 'filesystem' 就可以进行缓存的自定义配置。更多详情可以查看cache 官方文档
我们配置好,再次构建可以看到.cache/webpack 下生成了缓存文件

并且构建时间由856ms 缩短到 210ms

当然缓存也是不能盲目使用,也是需要斟酌,因为保存和读取这些缓存文件也会有一些时间开销,所以建议只对性能开销较大的 loader 采用改缓存优化。
优化构建结果
对于 优化构建结果 我们可以从 压缩代码、按需加载、提前加载、Code Splitting、Tree Shaking、Gzip、作用提升几个方面入手。

压缩代码
我们都知道,在浏览器中,运行 JS 代码是需要先将代码文件从浏览器通过服务器下载下来后再进行解析执行。那么在相同的网络环境下文件的大小会直接影响到网页加载的时长。那么,对代码进行压缩就是最简单高效的操作。
压缩 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
},
})
]
}
如上配置后,我们的html代码就会移除空格和注释。可以看到,重新构建后代码变成了一行。

压缩 css
对于webpack4及以下 使用的是 optimize-css-assets-webpack-plugin插件来压缩css。
在webpack5中推荐使用的是 css-minimizer-webpack-plugin。

使用optimize-css-assets-webpack-plugin插件的时候需要注意webpack版本,webpack4及以下的时候是配置在plugins里面。但是在webpack5中需要统一配置在optimization中。
下面笔者先来测试下optimize-css-assets-webpack-plugin
首先我们在入口文件里面引入test.css文件
import "./test.css";
test.css文件内容如下
.box {
background-color: aquamarine;
}
.item {
color: black;
}
然后配置好处理css的css-loader、mini-css-extract-plugin,这里需要注意,不能使用style-loader来处理css,而是需要使用mini-css-extract-plugin插件将css单独抽离出来才能进行压缩。
// webpack5
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
use: ["babel-loader"],
exclude: /node_modules/, //排除 node_modules 目录
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
exclude: /node_modules/, //排除 node_modules 目录
},
]
},
plugins: [
new MiniCssExtractPlugin()
],
optimization: {
// 是否需要压缩
minimize: true, // 开发环境需要开启
// 配置压缩工具
minimizer: [
// 添加 css 压缩配置
new OptimizeCssAssetsPlugin({}), // 需要安装
],
},
}
使用如上配置进行构建,可以看到,单独生成了 main.css 文件,并对css进行了压缩。

下面我们再来看看 css-minimizer-webpack-plugin的使用姿势。
配置方式基本相同
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
optimization: {
// 是否需要压缩
minimize: true, // 需要开启
// 配置压缩工具
minimizer: [
// 添加 css 压缩配置
new CssMinimizerPlugin({}), // 需要安装
],
},
我们重新构建,可以看到,单独生成了 main.css 文件,并对css进行了压缩。

压缩 js
在 webpack 中,我们可以使用 uglifyjs-webpack-plugin 和 terser-webpack-plugin 插件来优化 JS 资源。
我们先来看看uglifyjs-webpack-plugin的使用姿势
我们在入口文件定义如下代码
const names = ["randy", "jack"];
const say = (_name) => {
console.log(_name);
};
say(Math.random() > 0.5 ? names[1] : names[0]);
然后配置UglifyJsPlugin插件
const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); // 需要安装
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
use: ["babel-loader"],
exclude: /node_modules/, //排除 node_modules 目录
},
]
},
optimization: {
// 是否需要压缩
minimize: true, // 开发环境需要开启
// 配置压缩工具
minimizer: [
new UglifyJsPlugin({})
],
},
}
构建查看构建后的产物,可以看到代码被压缩了。

不过uglifyjs-webpack-plugin到目前已经有四五年没更新并且仓库也已存档不维护了,所以我们简单了解即可。
目前的主流还是terser-webpack-plugin,在webpack5生产环境中(mode=production),已默认开启。
我们不对optimization做任何配置,只设置mode=production进行构建,构建结果如下,可以验证在生产环境下会默认启用terser-webpack-plugin插件。

如果你不想使用terser-webpack-plugin插件的默认配置,想自定义,也是支持的,我们直接引入terser-webpack-plugin进行自定义配置即可。
const TerserPlugin = require("terser-webpack-plugin"); // webpack5内置,不需要再单独安装
optimization: {
// 是否需要压缩
minimize: true,
// 配置压缩工具
minimizer: [
new TerserPlugin({// 在这里自定义配置}),
],
},
这里有个细节需要注意,生产环境会默认配置terser-webpack-plugin,所以如果你还有其它压缩插件使用的话需要将TerserPlugin显示配置或者使用...,否则terser-webpack-plugin会被覆盖。
const TerserPlugin = require("terser-webpack-plugin"); // webpack5内置,不需要再单独安装
optimization: {
// 是否需要压缩
minimize: true,
// 配置压缩工具
minimizer: [
new TerserPlugin({}), // 显示配置
// "...", // 或者使用展开符,启用默认插件
// 其它压缩插件
new CssMinimizerPlugin(),
],
},
压缩 image
一般来说在打包之后,一些图片文件的大小是远远要比 js 或者 css 文件要来的大,所以我们首先要做的就是对于图片的优化,我们可以手动的去通过线上的图片压缩工具,如 tiny png 帮我们来压缩图片。
但是这个比较繁琐,在项目中我们希望能够更加自动化一点,自动帮我们做好图片压缩,这个时候我们就可以借助 image-webpack-loader 帮助我们来实现。它是基于 imagemin 这个 Node 库来实现图片压缩的。
使用很简单,我们只要在 file-loader 之后加入 image-webpack-loader 即可:
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif|jpeg|webp|svg)$/,
use: [
"file-loader",
{
loader: "image-webpack-loader",
options: {
mozjpeg: {
progressive: true,
},
// optipng.enabled: false will disable optipng
optipng: {
enabled: false,
},
pngquant: {
quality: [0.65, 0.9],
speed: 4,
},
gifsicle: {
interlaced: false,
},
},
},
],
exclude: /node_modules/, //排除 node_modules 目录
},
]
},
}
我们在件引入事先准备好的图片
import logo from "./logo.png";
console.log(logo);
这张图片原始大小为7kb

配置好 image-webpack-loader ,再次构建可以看到图片变成了3kb,压缩效果还是很明显的。

按需加载
很多时候我们不需要一次性加载所有的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());
}
再次构建可以看到,构建出来的文件名就是我们事先定义好的名称

写过vue的同学可能非常清楚,在vue-router里面,实现路由懒加载使用的最多的就是import()。
懒加载实际上就是 import 的语法,他不是 webpack 的功能,而是 ECMAScript 的语法,webpack 做的只是识别这种语法并应用。
提前加载(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'。
这样,页面首次加载的时候,浏览器空闲的会后会提前加载impModule.js。当我们点击按钮的时候,会直接从缓存中读取该文件,因此速度非常快。

preload
/* webpackPreload: true */使用方式类似。
prefetch 与 preload 的区别
preload chunk会在父chunk加载时,以并行方式开始加载。prefetch chunk会在父chunk加载结束后开始加载。preload chunk具有中等优先级,并立即下载。prefetch chunk在浏览器闲置时下载。preload chunk会在父chunk中立即请求,用于当下时刻。prefetch chunk会用于未来的某个时刻。- 浏览器支持程度不同,需要注意。
最后官网还告诉我们,错误地使用webpackPreload实际上会损害性能,因此在使用时要小心。
Code Splitting (代码分割)
在项目中,一般是使用同一套技术栈和公共资源。那么如果每个页面的代码中都有这些公开资源,是不是就会导致资源的浪费呢?在每一个页面下都会加载重复的公共资源,一是会浪费用户的流量,二是不利于项目的性能,造成页面加载缓慢,影响用户体验。
基本思路就是我们先要确定哪些是我们项目中使用内容长期不会更改的三方库(react、react-dom 等)和我们团队内部自己封装的公共 JS(util.js 等)。然后将其提取出放入到一个公共文件 common.js 中。这样,只要不升级基础库的版本,那么 common.js 文件的内容就不会变化,在访问页面的时候,就可以一直使用浏览器缓存中的资源。
在正式讲代码分割前,先要理解webpack的提出的几个概念,module、chunk和bundle。
module:每个import引入的文件就是一个模块 chunk:当module源文件传到webpack进行打包时,webpack会根据文件引用关系生成chunk bundle:是对chunk进行压缩、分割等处理后的产物

CommonChunkPlugin (已过时)
CommonChunkPlugin 主要应用在webpack3 中,在 webpack4 的时候已经被移除了。所以 webpack5 也不支持了,这里笔者就不再演示了,了解即可。

SplitChunksPlugin
下面笔者来看看取代 CommonChunkPlugin 的 SplitChunksPlugin
默认配置如下:
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', // 值有 `all`,`async` 和 `initial`
minSize: 20000, // 生成 chunk 的最小体积(以 bytes 为单位)。
minRemainingSize: 0,
minChunks: 1, // 拆分前必须共享模块的最小 chunks 数。
maxAsyncRequests: 30, // 按需加载时的最大并行请求数。
maxInitialRequests: 30, // 入口点的最大并行请求数。
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\/]node_modules[\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
默认情况下,它只会影响到按需加载的 chunks。
既然js支持代码分割,那css是不是也支持呢?
ExtractTextPlugin (已过时)
extract-text-webpack-plugin 在webpack3之前使用的很广泛,但是在webapck4+中已经不再使用了,仓库也在2019年存档了,不再更新了。
所以这个插件我们了解即可,就不再演示了。
webapck4+ 推荐使用的是mini-css-extract-plugin插件。
MiniCssExtractPlugin
我们知道,style-loader会把css打包进js文件里面,这样js在运行的时候才会在html文件动态生成style标签将样式插入,这无疑加大了我们js文件的体积。其次还不利于css代码的复用。
我们可以利用 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(),
}),
],
我们再次构建,可以发现,生成了单独的main.css文件。

达到了样式分离的效果。
css代码的分离优化原理其实和js是一样的。第一就是拆出来利用浏览器并发请求特性进行快速加载,其次就是多页面如果用到了相同样式能进行复用。
注意,此插件为每个包含 css 的 js 文件创建一个单独的 css 文件。
这句话的意思就是,css代码的分割不是以引入了多少个css文件构建后就有多少个css文件,而是根据你的js包来的,如果构建后你的js包只有一个那么css包也只会有一个,而不管你源代码里引入了多少个css文件。
Tree Shaking (摇钱树)
Tree Shaking又称为摇钱树,主要用来清除没有使用到的js代码。
JS tree shaking
下面笔者举个例子
我们先定义一个模块,导出加、减两个方法
// treeshak.js
export const increase = (a, b) => {
return a + b;
};
export const decrease = (a, b) => {
return a - b;
};
在入口文件引入,但是我们只使用其中一个方法。
// index.js
import { increase } from "./treeshak";
console.log(increase(1, 2));
我们以开发模式mode: "development"构建一下,可以发现我们没有使用的decrease方法也被打包进来了。

这肯定不是我们想要的,我们需要的是没有使用的代码踢除掉,我们配置下。
// webpack.config.js
optimization: {
usedExports: true
},
并在package.json里面配置sideEffects,表示对所有的文件都启用 tree shaking。
// package.json
"sideEffects": false
我们再次构建,发现decrease方法还是在里面,但是多了一句注释

他是unused的导出,意思就是这个方法是没被使用的。
在开发模式下并不会直接删除未使用的代码,而是会加上一个unused注释,如果直接删除的话,可能会影响我们开发时定位错误。
其实tree shaking在生产模式下已经默认开启了。我们使用mode: "production"重新构建下

可以发现,decrease被移除了。
上面我们配置sideEffects直接配置的是false,其实是不严谨的,因为有些导入我们是不需要 tree shaking。
比如我们导入样式,导入polyfill。
import './index.less'
import '@babel/polly-fill'
这种只导入未使用的代码也会被tree shaking,这样就是导致样式和polyfill都会丢失,这肯定不是我们想要的。所以我们需要进一步配置sideEffects。
"sideEffects": [
"*.less",
"@babel/polly-fill",
]
这里面的意思就是,我们碰到上面的几个模块,我们就不去进行 tree shaking。如果还有其它类似需求,在这里配置即可。
局限性
- 只对
ESM生效,对其他模块规范无效。 - 只能是静态声明和引用的
ES6模块,不能是动态引入和声明的。 - 只能处理模块级别,不能处理函数级别的冗余。
- 只能处理
JS相关冗余代码,不能处理CSS冗余代码。
CSS tree shaking
既然js能进行tree shaking,那css可以吗?
当然也是可以的,但是需要借助purgecss-webpack-plugin插件。
我们在index.html使用我们上面创建的index.less里面的.a但是.b我们并没有使用。
<div id="root">
<button id="btn1">懒加载</button>
<div class="a">有使用样式</div>
<div>没有使用样式</div>
</div>
我们直接构建,可以发现,.b我们没有使用到但是他还是被打包进来了

接下来我们配置下purgecss-webpack-plugin插件。
const path = require("path");
const PurgecssPlugin = require("purgecss-webpack-plugin");
const glob = require("glob"); // 文件匹配模式
plugins: [
// ...
new PurgecssPlugin({
// 这里我的样式在根目录下的index.html里面使用,所以配置这个路径
paths: glob.sync(`${path.join(__dirname)}/index.html`, { nodir: true }),
}),
]
我们再次构建,可以看到.b样式没有被打包进来。

达到了跟js tree shaking一样的效果。
Gzip
前端除了在打包的时候将无用的代码或者 console、注释剔除之外。我们还可以使用 Gzip 对资源进行进一步压缩。Gzip 原本是 UNIX 系统的文件压缩,后来逐步成为 web 领域主流的压缩工具。那么浏览器和服务端是如何通信来支持 Gzip 呢?
- 当用户访问 web 站点的时候,会在
request header中设置accept-encoding:gzip,表明浏览器是否支持Gzip。 - 服务器在收到请求后,判断如果需要返回
Gzip压缩后的文件那么服务器就会先将我们的JS\CSS等其他资源文件进行Gzip压缩后再传输到客户端,同时将response headers设置content-encoding:gzip。反之,则返回源文件。 - 浏览器在接收到服务器返回的文件后,判断服务端返回的内容是否为压缩过的内容,是的话则进行解压操作。
一般情况下我们并不会让服务器实时 Gzip 压缩,而是利用webpack提前将静态资源进行Gzip 压缩,然后将Gzip 资源放到服务器,当请求需要的时候直接将Gzip 资源发送给客户端。
我们只需要安装 compression-webpack-plugin 并在plugins配置就可以了
const CompressionWebpackPlugin = require("compression-webpack-plugin"); // 需要安装
module.exports = {
plugins: [
new CompressionWebpackPlugin()
]
}
配置好我们再来构建,可以发现生成了资源的.gz文件

作用提升 (Scope Hoisting)
Scope Hoisting 是 webpack3 的功能,翻译过来的意思是“作用域提升”。在 JavaScript 中,也有类似的概念,“变量提升”、“函数提升”,JavaScript 会把函数和变量声明提升到当前作用域的顶部,Scope Hoisting 也是类似。webpack 会把引入的 js 文件“提升”顶部。
在没有使用 Scope Hoisting 的时候,webpack 的打包文件会将各个模块分开使用 __webpack_require__ 导入,在使用了 Scope Hoisting 之后,就会把需要导入的文件直接移入使用模块的顶部。这样做的好处有
- 代码中函数声明和引用语句减少,减少代码体积
- 不用多次使用
__webpack_require__调用模块,运行速度会的得以提升。
所以,Scope Hoisting 可以让 webpack 打包出来的代码文件体积更小,运行更快。Scope Hoisting 的原理也很简单,主要是其会分析模块之间的依赖关系,将那些只被引用一次的模块进行合并,减少引用的次数。
因为 Scope Hoisting 需要分析模块之间的依赖关系,所以源码必须采用 ES6 模块化语法。也就是说如果你使用非 ES6 模块或者使用 import() 动态导入的话,则不会有 Scope Hoisting。
Scope Hoisting 是 webpack 内置功能,只需要在plugins里面使用即可
module.exports = {
plugins: [
// 开启 Scope Hoisting 功能
new webpack.optimize.ModuleConcatenationPlugin()
]
}
当然,在 webpack4+ 中还可以在optimization中通过参数concatenateModules直接配置
module.exports = {
optimization: {
concatenateModules: true // 开启 Scope Hoisting 功能
},
}
不过生产环境下 Scope Hoisting 功能是默认开启的。我们不用再额外处理。
常用分析工具
前面介绍了很多优化打包的方式,但是怎么能够定位我们项目的问题在哪?又怎么去检验我们的优化成果呢?
最直接的分析方式当然是查看我们每次打包后在控制台输出的结果,例如

但是这样的输出结果的可读性非常差并且不直观。我们可以使用可视化分析工具更简单、直观的查看打包结果,方便分析和排查问题。
下面笔者介绍两款比较好用的分析工具
第一个是时间分析工具是 speed-measure-webpack-plugin
第二个是构建结果产物分析工具 webpack-bundle-analyzer
时间分析工具 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 插件应该是迄今为止使用最多的 webpack 可视化分析工具。
webpack-bundle-analyzer 能可视化的反映
- 打包出的文件中都包含了什么;
- 每个文件的尺寸在总体中的占比,哪些文件尺寸大,思考一下,为什么那么大,是否有替换方案,是否使用了它包含的所有代码;
- 模块之间的包含关系;
- 是否有重复的依赖项,是否存在一个库在多个文件中重复? 或者捆绑包中是否具有同一库的多个版本?
- 是否有相似的依赖库, 尝试使用一种依赖库实现相似的功能。
- 每个文件的压缩后的大小。
使用姿势如下:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; // 需要安装
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
在重新执行 build 命令就会发现浏览器自动打开了个窗口 http://127.0.0.1:8888/,展示本项目本次 build 的结果的可视化分析:

总结
系列文章
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!
转载自:https://juejin.cn/post/7244819106342780988