Webpack 5 发布后,构建性能优化领域出现了一个值得关注的设计理念:Altruism 利他主义。它并不是一个可以单独安装的插件,也没有独立的配置开关,而是体现在默认的依赖提取、公共模块共享和运行时代码分离等多个环节中。简单地说,Altruism 让模块更愿意把公共依赖贡献出来,形成可复用的 chunk,从而减少整体产物的重复代码和缓存失效范围。

一、Altruism 在依赖提取中的表现
Webpack 5 的 splitChunks 默认行为本身就带有明显的利他倾向。当一个公共模块被多个入口引用时,Webpack 不会简单地在每个入口中各自打包一份,而是会把它抽离到独立的 chunk 中。比如三个页面都引入了 lodash,如果没有这种机制,每个页面产物都会包含一份完整的 lodash 代码,最终用户访问不同路由时需要重复下载相同逻辑。利他主义的作用就是让 lodash 成为一个共享模块,入口包让出这部分体积,改成通过运行时引用来获取。
这种策略需要配合 optimization.splitChunks 的配置来精细控制。默认情况下,Webpack 5 对异步模块和同步模块都会应用分割规则,只要模块满足最小体积和最小引用次数。下面的配置展示了如何把 node_modules 中的第三方库统一抽离到 vendors chunk,同时让公共业务代码进入 common chunk:
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
maxInitialRequests: 10,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
common: {
minChunks: 2,
name: 'common',
priority: 5,
reuseExistingChunk: true
}
}
}
}
};
在这个配置里,test 字段使用正则匹配来自 node_modules 的模块。它不关心模块属于哪个入口,只要满足条件就放入 vendors。这种跨入口的贡献行为会减少每个入口的自包含程度,但换来的是更稳定的公共缓存和更小的总体积。需要注意的是,如果公共模块非常小,比如只有几行工具函数,强行抽离反而会增加请求数量,所以 minSize 和 minChunks 需要结合业务场景调整。
利他主义并不意味着无脑共享。Webpack 5 还会根据 maxInitialRequests 等参数控制入口并发请求数,避免把所有小模块都独立成 chunk。找到共享与请求成本之间的平衡,是使用这一特性时的关键判断。
二、模块联邦中的 shared 与利他策略
模块联邦是 Webpack 5 引入的重要能力,它允许不同应用在运行时加载彼此的模块。这里的 Altruism 体现得更直接:多个应用可以通过 shared 配置声明共享依赖,而不是各自携带完整的 React、Vue 或工具库。比如一个微前端架构中,主应用和子应用都使用 React,如果双方都把自己的 React 打进产物,不仅包体翻倍,还会在运行时出现多个 React 实例,导致 hooks 状态异常。
通过 ModuleFederationPlugin 的 shared 字段,依赖可以被提升为运行时共享模块。配置如下:
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js'
},
shared: {
react: { singleton: true, eager: false },
'react-dom': { singleton: true, eager: false }
}
})
]
};
shared 对象中的 singleton 表示整个运行时只允许存在一个 React 实例,即使版本不完全一致也会协商使用统一的版本。eager 为 false 时,依赖不会立即被打包到当前应用,而是等远程模块加载后再按需获取。这样主应用和远程应用就形成了一种利他关系:每个应用都放弃了对依赖的独占打包,转而依赖全局共享层。这种做法降低了重复下载,也解决了多实例冲突。
但是模块联邦的共享也有代价。过度共享会增加运行时协商复杂度,版本不一致时还可能产生兼容问题。因此在实际项目中,通常只共享 react、react-dom、vue 这类基础框架,业务库仍然保留在各自应用内。合理定义 shared 范围,是模块联邦落地时不可忽视的一步。
三、运行时分离与调优验证
除了公共依赖提取和联邦共享,Webpack 5 还会自动把 runtime 代码从入口中分离出来。runtime 负责模块加载、chunk 映射和热更新逻辑,它很小但变化频繁。如果 runtime 混在入口文件中,每次业务代码变更都会导致入口文件名变化,从而失去长效缓存。把 runtime 单独输出成 chunk,可以让入口文件保持稳定,这也是利他主义在缓存层面的一种表现。
开启方式非常简单:
module.exports = {
optimization: {
runtimeChunk: 'single'
}
};
这样所有入口共享同一个 runtime chunk。它的体积通常只有几 KB,但可以让业务入口的 contenthash 在公共模块不变时保持稳定。验证时可以使用 Webpack 的 stats 输出查看 chunk 构成:
npx webpack --profile --json > stats.json
通过对 stats.json 的分析,可以找到哪些模块被重复打包、哪些 chunk 体积异常大。如果发现同一个库在多个 chunk 中重复出现,就说明利他策略没有被正确触发,需要检查 splitChunks 的 test 和 minChunks 条件是否覆盖了该模块。反过来,如果共享过度,首屏请求数会明显增加,此时应适当提高 minSize 或降低 maxInitialRequests。
Altruism 利他主义并不是一个可以拿来即用的选项,它更像是一组默认策略和配置项共同作用的结果。理解了它在依赖提取、联邦共享和 runtime 分离中的具体表现,开发者就能更有针对性地调整构建配置,减少重复代码,提升应用加载性能和长期缓存命中率。

