Webpack 5 发布时,官方把改进归为速度、质量和能力三类。速度提升最容易感知,模块联邦这类能力也常被讨论,但 Quality 相关的变化对前端工程长期维护的影响其实更隐蔽、更持久。这里说的质量不是代码风格检查,而是指构建产物的确定性、冗余度和缓存有效性。比如同样改动一个业务组件,是否只有对应的 chunk 哈希变化,还是 vendor 文件也被迫失效;比如一段从未被引用的代码,能不能在压缩前就被移除。本文从这些具体场景出发,说明 Webpack 5 如何通过配置组合把质量控制在可预期范围内。

一、先厘清 Webpack 5 质量改进的边界
很多 Webpack 4 项目在长期维护后都会遇到同一个现象:新增一个入口文件、调整一次模块引入顺序,甚至只修改一个注释,最终生成的 chunk 文件名里的 hash 就大面积变化。结果用户在浏览器端原本可以命中的强缓存全部失效,发布之后 CDN 回源压力陡增。这不是构建速度问题,而是产物缺乏确定性的表现。Webpack 4 的模块 ID 默认采用自增数字分配,模块插入顺序一变,后续 ID 全部后移,依赖这些 ID 的 runtime 映射和 chunk 名称自然跟着变。
Webpack 5 把这类问题统一纳入质量维度去解决。它引入确定性的模块 ID 和 chunk ID,让相同内容的模块在不同次构建中尽量保持相同标识;同时把缓存从内存扩展到文件系统,让模块编译结果可以跨进程复用;在代码分割方面,splitChunks 的默认行为更贴近实际项目,tree shaking 也能识别更多未使用代码。理解这些机制之后,配置 Webpack 就不再只是把打包跑通,而是能主动控制产物边界和缓存行为。
如果把 Webpack 4 到 Webpack 5 的升级比作一次基建改造,那么速度提升是换了更快的机器,质量提升则是把仓库的货架编号、出入库流程和垃圾清理机制重新设计了一遍。前者收益会随着硬件变化而减弱,后者却能在后续每一次发版中持续发挥作用。
二、确定性 ID:让哈希只跟随内容变化
Webpack 5 中可以通过 optimization.moduleIds 和 optimization.chunkIds 控制 ID 生成策略。其中 deterministic 策略会基于模块路径、内容等生成稳定的短数字 ID,而不是依赖模块在依赖图中的顺序。这样当你新增一个业务模块,其他模块的 ID 不会整体后移,只有当模块自身内容或路径发生变化时,它对应的 ID 才可能改变。这个特性对生产环境的缓存策略非常关键,因为 contenthash 的稳定性会直接影响用户端缓存的命中率。
下面是一段基础配置,把模块 ID 和 chunk ID 都设置为 deterministic,并让输出文件名使用 contenthash:
module.exports = {
output: {
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js',
clean: true
},
optimization: {
moduleIds: 'deterministic',
chunkIds: 'deterministic'
}
};
仅设置 contenthash 还不够。Webpack 生成的 runtime 代码中包含了模块 ID 与 chunk 映射关系,如果在默认情况下 runtime 被打进入口文件,那么任何模块 ID 变化都会导致入口文件哈希更新。更合理的做法是把 runtime 单独拆出来,避免它干扰业务 chunk 的缓存。下一节会结合 splitChunks 一起说明。
还要注意,deterministic 策略并非完全固定不变。当模块路径发生改变,或者构建上下文中的某些因素变化时,ID 仍可能重新计算。因此它解决的是顺序抖动问题,而不是保证跨环境绝对一致。对大多数团队来说,这已经足够把一次改动的影响范围从全局缩小到局部。
三、持久化缓存:二次构建与 CI 复用的关键
Webpack 4 的缓存主要基于内存,每次启动进程都会重新解析和构建所有模块。即使只修改一个文件,整个依赖图也要重新走一遍。Webpack 5 新增的文件系统缓存允许把模块编译结果、依赖关系和解析结果序列化到磁盘,二次构建时可以直接复用未变化的模块数据。这意味着本地开发和 CI 环境都能获得明显的速度收益,同时也能减少重复的模块解析错误。
配置文件系统缓存非常简单:
module.exports = {
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
}
};
上面的 buildDependencies 很关键。它把配置文件本身加入缓存依赖,一旦 webpack 配置发生变化,缓存会自动失效并重新构建。否则可能出现配置已经改了,但缓存仍然使用旧结果的情况。除此之外,如果项目使用了自定义 loader 或插件,建议在 cache 中通过 name 或 version 区分缓存版本,例如 cache: { type: 'filesystem', name: 'prod-cache-v1' },这样当 loader 处理逻辑变化时可以手动更新版本号让缓存失效。
在 CI 环境中使用文件缓存时,需要把缓存目录持久化到构建缓存服务中。默认缓存目录是 node_modules/.cache/webpack,如果 CI 不支持跨任务保留该目录,那么每次冷启动的收益会大打折扣。对于容器化的构建流程,可以把这个目录配置到挂载卷,或者使用公司内部的远程缓存服务。缓存策略设计得当,构建质量会从可复制性上得到进一步保障。
四、splitChunks 与 runtimeChunk:让产物边界更干净
代码分割是影响产物质量的重要一环。如果所有依赖都打进一个入口文件,产物体积会膨胀,浏览器缓存也无法按模块粒度更新。Webpack 5 的 splitChunks 默认配置已经比较合理,但实际项目中通常需要针对 node_modules 单独提取 vendor 包,并把异步 chunk 的公共依赖抽出来,避免多个页面重复打包同一份库代码。
下面是一段常见配置,同时把 runtime 拆成单独文件:
module.exports = {
output: {
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js'
},
optimization: {
moduleIds: 'deterministic',
chunkIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
reuseExistingChunk: true
},
common: {
minChunks: 2,
name: 'common',
priority: 5,
reuseExistingChunk: true
}
}
}
}
};
加入 runtimeChunk: 'single' 之后,Webpack 的运行时代码会被单独输出为一个文件。这样当业务代码或 vendor 代码发生变化时,runtime 文件即使更新,也不会影响业务 chunk 和 vendor chunk 的哈希。浏览器仍然可以复用未变化的缓存,只有真正改动的那部分代码需要重新下载。这个配置虽然会多出一个请求,但换来的是更稳定的缓存行为和更小的更新增量。
此外,splitChunks.cacheGroups 中的 priority 决定了模块被分配到哪个组的优先级。第三方库通常要优先进入 vendor 组,公共业务代码则进入 common 组。还应注意 reuseExistingChunk 的用法,它可以避免同一个模块被打进多个 chunk。设置这些参数时最好结合 webpack-bundle-analyzer 分析产物结构,看是否存在重复模块或明显的体积异常。
五、tree shaking 与 sideEffects:减少产物体积的隐性质量
tree shaking 并不是 Webpack 5 才有的概念,但 Webpack 5 对这一能力做了更深度的实现。它支持对嵌套导出、export * from 等语法做更精确的分析,同时也会更可靠地读取 package.json 中的 sideEffects 标记。如果你的项目使用 ES Modules 编写业务代码,配合 sideEffects: false,未使用的导出就可以在压缩阶段前被安全删除。
比如在 package.json 中声明:
{
"name": "my-app",
"sideEffects": false
}
同时配置 Webpack:
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
sideEffects: true
}
};
但 sideEffects: false 需要谨慎使用。有些模块虽然没有导出,却会在导入时产生副作用,比如全局 CSS、Polyfill、事件注册等。如果误标为无副作用,这些代码会被 tree shaking 移除,导致页面样式丢失或功能异常。对于包含样式文件的包,可以使用数组形式排除 CSS 文件,例如 "sideEffects": ["*.css"],这样只有 CSS 文件被保留为有副作用,其余 JS 仍然可以安全摇树。
tree shaking 的效果不能只看打包后体积,还要关注构建产物中的模块依赖关系。可以通过 stats 输出分析 JSON,再结合可视化工具查看哪些模块被保留、哪些被移除。如果发现某个库始终无法被摇树,多数是因为该库使用了 CommonJS 导出,或者缺少 sideEffects 声明。此时可以优先选择提供 ES Module 版本的依赖,或在项目层面通过 alias 指向 ESM 文件。
六、一份偏向质量的 Webpack 5 配置模板
把前面提到的内容组合起来,可以得到一个以质量优先为目标的 Webpack 5 配置。它不一定适合所有项目,但覆盖了大多数中大型前端工程的缓存、分割和摇树需求。具体参数还需要根据项目结构和部署环境调整。
const path = require('path');
module.exports = {
mode: 'production',
entry: {
app: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js',
clean: true
},
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
},
name: 'webpack5-quality-cache-v1'
},
optimization: {
moduleIds: 'deterministic',
chunkIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
reuseExistingChunk: true
}
}
},
usedExports: true,
sideEffects: true
}
};
配置完成后,建议先跑一次干净构建,记录下各 chunk 的文件名和体积。然后修改一个业务组件,观察只有业务对应 chunk 的 hash 发生变化,vendor 和 runtime 是否保持稳定。再修改一次第三方库版本,确认 vendor chunk 的 hash 只跟随依赖变化而变化。通过这两步验证,可以快速判断质量配置是否真正生效。
Webpack 5 的质量改进并不是某一个开关能全部搞定的,它需要把确定性 ID、持久化缓存、代码分割和 tree shaking 组合起来使用。单独打开其中一项,效果可能有限。只有理解这些配置之间的联动关系,才能让构建产物在体积、稳定性和缓存命中率上同时达到一个更可靠的水平。