Webpack 5 的性能数据经常被描述得过于简单,比如只给一个构建时间下降的百分比。真正有价值的对比应该区分冷启动、热缓存构建、watch 重建,以及产物体积和哈希稳定性。因为在不同场景下,性能差异的构成完全不同。冷启动快来自缓存序列化与模块依赖图遍历优化,而热缓存构建快来自文件系统缓存直接跳过大量 loader 和解析步骤。只盯着某一次冷启动时间,容易得出 Webpack 5 提升有限的结论,但放到日常开发的反复构建场景中,差距会被放大数倍。

为了得出相对可靠的数据,测试使用同一套中型单页应用源码,包含约 120 个 JavaScript 模块、40 个样式文件以及 20 个图片资源。分别使用 Webpack 4.46 和 Webpack 5.74 运行生产构建,配置尽量保持一致,只有在 Webpack 5 中显式开启 filesystem 缓存。测试环境为同一台机器、同一个 Node.js 版本,并删除 node_modules/.cache 与 dist 目录后再执行冷启动对比。
Webpack 5 性能提升的底层原因
Webpack 4 的缓存主要依赖内存,构建结束后缓存就消失了,下次执行 webpack 命令时必须重新解析所有模块、重新运行 loader、重新生成 chunk。即使采用 cache-loader 或者 hard-source-webpack-plugin 这类外部插件,也很难覆盖所有编译阶段,而且容易与 splitChunks 等内置优化产生冲突。Webpack 5 则将缓存能力内置为 cache.type: 'filesystem',直接序列化模块依赖图、编译结果和 chunk 信息到磁盘。二次构建时,大部分模块不需要重新执行 loader,也不需要重新进行语法解析,只需要反序列化缓存并重新拼接 chunk 即可。
另一个关键改进来自代码生成阶段。Webpack 5 重构了 chunk 生成算法,减少了模块包装函数的重复代码,同时对作用域提升进行了更细粒度的控制。产出的 runtime 代码体积明显变小,这部分代码无论业务代码怎样变化,通常都会被单独输出为 runtime chunk。runtime 变小意味着客户端加载的公共代码更少,浏览器解析执行更快。此外,Webpack 5 对 sideEffects 的分析更加准确,能删除更多未使用的导出,tree shaking 的结果更接近手写的精简代码。
长期缓存逻辑也发生了质变。Webpack 4 中,即使使用 contenthash,由于 module id 和 chunk id 默认是自增数字,插入一个新模块可能导致多个 chunk 的哈希都发生变化。Webpack 5 通过 deterministic 的模块 ID 和 chunk ID 策略,把哈希变化范围缩小到真正内容变化的文件上。这意味着部署新版本时,用户只需要重新下载变动的几个 chunk,而不是整个 vendor 包都失效。
持久化缓存与长期缓存的实际对比
Webpack 5 的 filesystem 缓存配置非常简单,核心字段如下:
module.exports = {
mode: 'production',
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
}
};
其中 type: 'filesystem' 开启磁盘缓存,buildDependencies.config 用来声明哪些配置文件变化后需要失效缓存。这里传入 [__filename] 表示当前 webpack 配置文件变化时重新构建,避免因为修改了配置却仍然命中旧缓存。Webpack 4 没有这种内置能力,只能通过第三方插件模拟,稳定性和覆盖范围都差一截。
长期缓存方面,Webpack 5 推荐使用如下输出配置:
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js'
},
optimization: {
moduleIds: 'deterministic',
chunkIds: 'deterministic',
runtimeChunk: 'single'
}
moduleIds: 'deterministic' 会根据模块路径生成稳定的短 ID,即使模块顺序发生变化,ID 也不会漂移。chunkIds: 'deterministic' 同理,用于保证 chunk 名称稳定。runtimeChunk: 'single' 把 runtime 代码单独拆成一个文件,避免 runtime 变化影响业务 chunk 的 contenthash。如果迁移后仍然发现文件哈希频繁变化,多半是没有把这三项同时配置完整。
构建时间与产物体积的量化测试
冷启动测试中,不开启任何缓存,Webpack 4 完成生产构建用时约 41.2 秒,Webpack 5 用时约 36.5 秒,提升幅度约为 11%。开启 filesystem 缓存后,Webpack 5 首次构建约 37.1 秒,第二次构建直接降到约 2.3 秒。而 Webpack 4 由于每次启动都是冷缓存,构建时间始终维持在 41 秒左右。watch 模式下修改一个普通业务模块,Webpack 5 的重建时间约为 0.8 秒,Webpack 4 约为 1.9 秒,差距达到两倍以上。
产物体积方面,同样开启 tree shaking 和 splitChunks 后,Webpack 5 生成的 JavaScript 总量约为 412 KB,Webpack 4 约为 438 KB,体积下降约 6%。runtime 代码从 6.4 KB 降到 4.8 KB。虽然体积下降幅度不像构建速度那么夸张,但配合稳定的 contenthash,部署后客户端需要实际重新下载的字节数会少得多。例如只修改一个工具函数时,Webpack 4 可能导致 vendor chunk 和多个异步 chunk 哈希全部变化,而 Webpack 5 通常只有对应业务 chunk 发生变化。
下面的表格汇总了同一项目在两类场景下的关键数据:
| 对比项 | Webpack 4 | Webpack 5 |
|---|---|---|
| 冷启动构建时间 | 41.2 秒 | 36.5 秒 |
| 二次构建时间 | 41 秒左右 | 2.3 秒 |
| watch 重建时间 | 1.9 秒 | 0.8 秒 |
| JavaScript 总体积 | 438 KB | 412 KB |
| runtime 体积 | 6.4 KB | 4.8 KB |
| contenthash 稳定性 | 较差 | 稳定 |
升级后容易忽视的性能配置陷阱
仅仅把 webpack 版本升到 5 并不代表性能自动变好。很多团队升级后发现构建速度没有明显变化,甚至 vendor 包被拆得异常分散,原因在于 splitChunks 的默认行为发生了变化。Webpack 5 对 cacheGroups 的处理更精细,如果项目之前依赖 Webpack 4 的默认策略,迁移后应该重新审视自己的 splitChunks 配置,尤其是 priority 和 reuseExistingChunk 的影响。
一个常见的优化是显式把 React、ReactDOM 等基础库拆成高优先级缓存组,避免它们和其他第三方库混在一起导致哈希频繁联动。配置示例如下:
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react-vendor',
priority: 20
},
libs: {
test: /[\\/]node_modules[\\/]/,
name: 'libs',
priority: 10
}
}
}
}
这里 priority: 20 确保 react 相关模块优先进入 react-vendor 组,而不是被更宽泛的 libs 组捕获。合理的分组可以让业务变化不牵动基础库的哈希,长期缓存收益才能体现出来。另一个容易忽略的问题是旧缓存目录没有清理。升级后如果发现构建结果异常,先删除 node_modules/.cache 再重新构建,往往能解决大部分诡异问题。
最后需要留意 [hash] 和 [contenthash] 的区别。前者基于整个构建过程生成,任何变化都会改变所有文件的文件名;后者基于文件内容生成,只有内容本身变了才会改变文件名。Webpack 5 虽然对 contenthash 做了改进,但前提是模块 ID 和 chunk ID 都采用确定性策略。如果这三项没有同时配置,长期缓存依然会出现大面积失效。迁移时建议先跑通构建,再通过两次构建的产物文件名对比,确认只有变更模块对应的 chunk 哈希发生了变化。

