Webpack 5 的构建性能优化往往被简化为“加 cache、换 loader”,但如果没有一套可复现的 Benchmarks 基准测试流程,任何优化都缺少可信的对比依据。基准测试的核心不是跑出最快的数字,而是让每次改动能被精确度量,从而判断改动是否真正有效。

先从最常见的场景说起:项目构建从 30 秒涨到 60 秒,开发者通常第一反应是“某个依赖变大了”或者“机器卡了”。这种判断往往不可靠。Webpack 5 内置的 stats 字段可以输出模块数量、chunk 大小、警告错误等信息,但它不会直接给出每个 loader 和 plugin 的耗时占比。真正要做基准测试,需要把构建过程拆成多个可观测阶段,并保证每次测试的初始条件一致。下面依次说明具体做法。
一、基准测试前必须固定的变量
基准测试最容易犯的错误,是在不同机器状态或不同缓存条件下比较构建时间。比如第一次构建没有缓存,第二次构建命中了 Webpack 5 的持久化缓存,时间自然大幅下降,但这不代表配置优化起了作用。要让数据可对比,至少要固定 Node 版本、Webpack 版本、操作系统、CPU 负载和磁盘类型。
常见的做法是写一个基准测试脚本,在每次测试前删除 node_modules/.cache 和 dist 目录,并使用 --no-cache 或配置 cache: false 来模拟冷启动。如果测试热启动,则需要先执行一次完整构建,再连续执行多次构建取平均值。下面是一个简单的冷启动基准脚本:
#!/usr/bin/env bash # 固定 Node 版本建议通过 nvm use 18 或 .nvmrc 控制 rm -rf node_modules/.cache dist node -e "console.log(process.version)" time npm run build
这里的 time 命令只能拿到整个命令的总耗时,无法分析内部细节,但适合做快速对比。如果希望获得更细粒度的数据,可以在 Webpack 配置中开启 stats: 'detailed' 并记录 stats.toJson() 中的时间字段。不过 stats 对 loader 阶段的时间划分比较粗糙,需要配合专门的插件。
二、使用 speed-measure-webpack-plugin 采集阶段耗时
speed-measure-webpack-plugin 是基准测试中常用的插件,它可以包裹 Webpack 配置,并在构建结束后输出每个 loader 和 plugin 的耗时、速度以及相对占比。安装后只需要在 webpack.config.js 的外层用 smp.wrap() 包住原来的配置即可。下面是一个基础示例:
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin({
outputFormat: 'human',
loaderTopFiles: 10,
pluginTopFiles: 5
});
module.exports = smp.wrap({
mode: 'production',
entry: './src/index.js',
output: {
path: require('path').resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
clean: true
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader']
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
});
运行构建后,终端会输出类似 smp General output time took 34.2 secs 的摘要,并列出 babel-loader、css-loader 等各自耗时。需要注意的是,该插件会改变 Webpack 内部的部分执行逻辑,可能对构建时间产生轻微额外开销,因此它适合作为分析工具,不适合作为最终性能验收的唯一标准。测得的百分比用于定位瓶颈,而不是与其他项目的绝对数字比拼。
如果项目使用 Webpack 5 的持久化缓存,speed-measure-webpack-plugin 在热启动场景下可能会因为缓存命中而几乎不触发 loader,导致数据看起来异常低。这时应区分冷启动和热启动两组数据,分别观察首次构建与增量构建的耗时分布。冷启动关注 loader 和 plugin 的初始化成本,热启动关注缓存写入、依赖追踪和产物生成的成本。
三、结合官方 stats 与自定义计时采集模块级数据
除了 loader 和 plugin 的耗时,Webpack 5 的 stats 对象还包含 modules、chunks、assets 等详细信息。通过 stats.toJson({ all: false, timings: true, modules: true }) 可以拿到每个模块的构建耗时、大小和依赖关系。对于大型项目,这些数据可以用来找出耗时最长的单个模块,例如某个依赖包解析非常慢,或者某个文件触发了大量 loader 转换。
下面是一段 Node 脚本示例,在构建完成后读取 stats 并输出耗时前 10 的模块:
const webpack = require('webpack');
const config = require('./webpack.config.js');
webpack(config, (err, stats) => {
if (err) {
console.error(err);
return;
}
const json = stats.toJson({
all: false,
timings: true,
modules: true,
reasons: false
});
const modules = json.modules || [];
const sorted = modules
.filter((m) => m.profile && m.profile.building)
.sort((a, b) => b.profile.building - a.profile.building)
.slice(0, 10);
sorted.forEach((m) => {
console.log(`${m.name} - ${m.profile.building} ms`);
});
});
代码中的 m.profile 只有在配置了 profile: true 时才会生成,因此需要在 Webpack 配置中加上:
module.exports = {
// 其他配置...
profile: true,
stats: {
timings: true,
modules: true
}
};
这种自定义采集方式比直接看终端输出更灵活,可以把数据写入 JSON 文件,再通过脚本生成多次运行的均值和标准差。基准测试最忌讳只看一次结果,因为第一次运行可能受到文件系统缓存、CPU 频率波动等因素影响。建议至少连续执行 5 到 7 次,去掉最高值和最低值后取平均。
四、Webpack 5 持久化缓存下的冷热启动对比
Webpack 5 一个显著变化是内置了持久化缓存,不再需要 hard-source-webpack-plugin 这类第三方方案。开启方式很简单:
module.exports = {
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
}
};
持久化缓存会把模块和 chunk 的编译结果写入 node_modules/.cache/webpack,第二次构建时可以直接复用,从而显著降低冷启动后的重复工作。在基准测试中,必须分别记录开启缓存与关闭缓存两种模式的数据。关闭缓存时,构建时间主要由模块解析和编译决定;开启缓存后的首次构建可能比关闭缓存稍慢,因为需要额外写入缓存文件,但第二次构建会有大幅下降。
一个容易忽略的干扰因素是 buildDependencies。Webpack 会根据这个配置决定哪些文件变化时缓存失效。如果 webpack.config.js 没有正确声明,修改配置后可能不会重新构建,导致基准测试结果看似稳定但实际上已经失效。建议在每次修改配置后清空一次缓存,确保测试的是当前配置的完整效果。
五、常见误区与数据解读建议
不少开发者看到构建时间下降就认为优化成功,但下降可能是因为并行任务数量变化、机器刚重启、或者测量脚本本身存在预热漏洞。比较可靠的做法是记录每次测试时的系统负载,例如通过 os.loadavg() 获取 CPU 队列长度,如果负载超过 CPU 核心数,测出来的数据就不具备参考价值。
另一个误区是过度关注 loader 耗时而忽略 resolve 和 plugin 阶段。例如 ts-loader 耗时高可能不是 loader 本身慢,而是 TypeScript 的 transpileOnly 没有开启,或者 resolve.extensions 配置了过多后缀导致每次解析都要尝试多种文件。通过 stats 和 speed-measure 交叉对比,才能定位到具体瓶颈。
最后,基准测试不是一次性的。建议把测试脚本、配置和结果记录纳入仓库,设置 CI 任务在相同容器中定期运行,这样才能发现依赖升级或配置改动带来的性能回归。数据只有连续可比,才能成为性能优化的可靠依据。