共享依赖 Tree Shaking
背景
在使用 Module Federation 时,我们常常通过 shared 配置来共享通用依赖(如 antd, React 等),以减少重复打包和提升应用间的一致性。然而,传统 shared 机制存在一个痛点:它总是会打包并提供完整的依赖包,即便你的应用实际只用到了其中的一小部分功能(例如,只用了一个 antd 的 Button 组件)。
这会导致:
- 构建产物体积过大:不必要的代码被打包,增加了最终产物的体积。
- 运行时性能损耗:浏览器需要下载并执行更多非必需的 JavaScript,延长了页面加载和渲染时间。
Shared Tree Shaking 正是为了解决这一问题而生。它能够智能地分析你的代码,精确识别出实际被使用的模块导出(exports),并仅打包这部分代码。最终,你的应用将获得一个经过“摇树”优化、体积更小的共享依赖。
- 更小的构建体积:从源头上减少不必要的代码,大幅缩减产物尺寸。
- 更快的加载速度:用户只需下载真正需要的代码,加速页面响应。
- 更优的渲染性能:减少浏览器解析和执行的 JavaScript,提升运行时体验。
快速上手
为你的项目开启 Shared Tree Shaking 非常简单,只需在配置文件中添加 treeShaking 选项即可。
示例
本地验证
在本地构建后,观测对应的共享依赖(antd)产物体积以及导出内容,期望的内容是只包含使用的模块。
配置项
核心配置为 sharedItem.treeShaking,你也可以通过设置以下配置来控制 Tree Shaking 产物路径:
- treeShakingDir
- injectTreeShakingUsedExports
- treeShakingSharedPlugins
- treeShakingSharedExcludePlugins
模式切换
我们提供了两种模式来以适应不同团队和项目的需求。
runtime-infer
runtime-infer 是一种轻量级的、无需中心化服务支持的策略。
- 如何启用:将
mode设置为runtime-infer。 - 适用场景:
- 本地开发环境,快速验证效果。
- 没有部署或 CI 服务的团队。
- 规模较小、消费者单一的项目。
- 工作逻辑:运行时,如果当前页面需要的功能已经被某个已加载的、经过 Tree Shaking 的共享包所满足,则直接复用。否则,回退加载完整的依赖包,以确保功能完备。
- 提升复用率:由于缺少全局视角,
runtime-infer的复用率可能不高。你可以通过手动在配置中补充usedExports列表,提前声明可能会用到的模块,从而引导编译器生成更优化的共享包,提升复用效率。
server-calc
server-calc 是强烈推荐的最佳实践。它通过中心化的服务来最大化 Tree Shaking 的收益,实现全局最优。
自建 Deploy Server
如果你打算自建部署服务(Deploy Server),可以参考下面这套流程来落地 server-calc。
-
部署阶段:汇总
usedExports在 CI/CD 部署流程中,你的部署服务需要:- 收集所有即将上线应用的构建元信息(通常是
mf-stats.json或同类文件)。 - 针对同一份共享依赖(例如
antd@6.1.0),汇总各应用声明的usedExports列表。
- 收集所有即将上线应用的构建元信息(通常是
-
计算全局最小并集 合并并去重所有应用的
usedExports,得到该共享依赖在全局范围内必需模块的最小集合(并集)。 -
触发二次构建(生成优化共享包) 调用构建工具(如 Rspack CLI),以独立构建的方式传入关键信息:
- 目标共享依赖的名称与版本(如
antd@6.1.0)。 - 上一步得到的全局
usedExports最小并集。 构建完成后,产出一个仅包含这些模块、体积更小的独立共享包。
- 目标共享依赖的名称与版本(如
-
更新并下发 Snapshot 将新生成的优化包上传到静态资源服务器(CDN)。然后,更新模块联邦的
Snapshot文件(通常是远程的mf-manifest.json),写入或更新以下关键字段:secondarySharedTreeShakingEntry: 指向新生成的优化包的 URL。secondarySharedTreeShakingName: 优化包的唯一名称。treeShakingStatus: 标记为可用状态。 最后,将更新后的Snapshot文件下发给所有消费者应用。
-
运行时按需加载 当用户访问应用时,MF Runtime 会读取
Snapshot。如果检测到treeShakingStatus可用且满足加载条件,它会自动加载secondarySharedTreeShakingEntry指向的优化包,而不是全量包。
版本、缓存与兼容性建议:
- 严格版本管理是策略的基石。确保二次构建与运行时加载都基于精确版本号(如
antd@6.1.0),避免不匹配。 - 为二次构建产物设置合理缓存策略。由于内容会随全局
usedExports变化,建议使用基于内容哈希的命名,实现长期缓存。 - 该流程天然向前兼容:未启用或不满足条件时仍加载全量包,不影响现有功能。
验证与回退
如何确认 Tree Shaking 生效?
- 网络面板检查:在浏览器开发者工具的“网络 (Network)”面板中,筛选加载的 JS 文件。确认加载的是二次构建生成的、带有特定标识(如
secondary或哈希值)的优化包,而不是原始的全量包。其体积也应该显著小于全量包。 - 产物内容比对:直接下载并查看加载的共享依赖 JS 文件内容。搜索你未使用的组件或函数名(例如,你只用了
Button,可以去搜索Modal),如果搜索不到,说明它们已被成功摇掉。 - Chrome DevTools 验证:在 Chrome DevTools 的「Shared」面板中选中目标共享依赖,查看其状态标签:出现
Tree Shaking Loaded表示 Tree Shaking 生效并加载了裁剪产物;仅有Loaded则表示回退为全量包加载;Tree Shaking Loading表示正在按 Tree Shaking 路径加载。

如何安全回退?
共享依赖 Tree Shaking 的设计包含了自动安全回退机制。如果运行时检测到任何问题(如 Snapshot 未下发、网络错误、版本不匹配等),它会默认加载全量的共享依赖包,确保应用的稳定性。
如果你需要手动禁用此功能,只需在部署服务中停止二次构建和 Snapshot 更新流程即可。
生成二次构建(secondary)共享产物
如果你的诉求是“让单个应用按需裁剪共享依赖”,通常只需要配置 usedExports。
本节介绍的是生成一份可被运行时按需加载的独立共享产物(secondary),用于在部署侧统一下发或复用。
该过程只构建共享依赖,remotes 不会生效,因此构建前请确保项目入口(entry)只引入了共享依赖。
非 Modern.js 项目
在构建配置中使用 TreeShakingSharedPlugin,设置 secondary: true,并复用现有的 mfConfig。
Modern.js 项目
当你使用 @module-federation/modern-js-v3 插件时,可以在插件配置中设置 secondarySharedTreeShaking: true 来开启二次构建产物生成。
常见问题 (FAQ)
1. Shared Tree Shaking 能和 eager: true 一起使用吗?
不能。 eager: true 会将共享依赖直接打包进应用入口文件 (initial chunk),这与 Tree Shaking 按需动态加载的机制是互斥的。你需要在这两者之间做出取舍:
- 若共享依赖体积不大,且追求极致的初始加载速度,可考虑
eager: true。 - 若共享依赖体积庞大(如组件库),强烈建议关闭
eager,使用 Tree Shaking 来获得显著的体积与性能收益。
2. 在 runtime-infer 模式下,单例依赖需要注意什么?
需要特别小心。如果一个共享依赖被配置为 singleton: true(必须全局单例),可能会出现以下场景:
- 应用 A 只用到了
antd的Button,加载了自己的树摇包。 - 应用 B 用到了
antd的Modal,它会加载自己的全量包。
此时,页面上会同时存在两个不同版本的 antd 实例(一个极简版,一个完整版),这会破坏单例模式,可能导致样式冲突、状态不共享、甚至应用崩溃。
建议:对于必须保持单例的库,优先使用 server-calc 策略,确保所有消费者都使用同一个经过全局优化的共享包。如果只能使用 runtime-infer,请通过补充 usedExports 尽可能地让生成的包更完整,以降低冲突风险。
3. Tree Shaking 的命中条件是什么?
主要依赖于构建工具的静态分析能力。代码必须采用 ES Module (import/export) 语法,以便编译器能够分析出哪些导出被使用了。CommonJS (require/module.exports) 模块通常无法被有效 Tree Shaking。
4. 它是否会破坏已发布项目的共享依赖?
不会。 Tree Shaking 的数据源和加载路径与原有的共享机制是隔离的,并且有严格的命中条件和安全回退逻辑。它不会影响已经稳定运行的老项目。