共享依赖 Tree Shaking

背景

在使用 Module Federation 时,我们常常通过 shared 配置来共享通用依赖(如 antd, React 等),以减少重复打包和提升应用间的一致性。然而,传统 shared 机制存在一个痛点:它总是会打包并提供完整的依赖包,即便你的应用实际只用到了其中的一小部分功能(例如,只用了一个 antd 的 Button 组件)。

这会导致:

  • 构建产物体积过大:不必要的代码被打包,增加了最终产物的体积。
  • 运行时性能损耗:浏览器需要下载并执行更多非必需的 JavaScript,延长了页面加载和渲染时间。

Shared Tree Shaking 正是为了解决这一问题而生。它能够智能地分析你的代码,精确识别出实际被使用的模块导出(exports),并仅打包这部分代码。最终,你的应用将获得一个经过“摇树”优化、体积更小的共享依赖。

核心收益
  • 更小的构建体积:从源头上减少不必要的代码,大幅缩减产物尺寸。
  • 更快的加载速度:用户只需下载真正需要的代码,加速页面响应。
  • 更优的渲染性能:减少浏览器解析和执行的 JavaScript,提升运行时体验。

快速上手

为你的项目开启 Shared Tree Shaking 非常简单,只需在配置文件中添加 treeShaking 选项即可。

示例

rspack.config.ts
export default {
  //...
  plugins: [
    new ModuleFederationPlugin({
      //...
      shared: {
        'antd': {
          treeShaking: { mode: 'runtime-infer' }
        }
      }
    })
  ]
};

本地验证

在本地构建后,观测对应的共享依赖(antd)产物体积以及导出内容,期望的内容是只包含使用的模块。

配置项

核心配置为 sharedItem.treeShaking,你也可以通过设置以下配置来控制 Tree Shaking 产物路径:

模式切换

我们提供了两种模式来以适应不同团队和项目的需求。

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

  1. 部署阶段:汇总 usedExports 在 CI/CD 部署流程中,你的部署服务需要:

    • 收集所有即将上线应用的构建元信息(通常是 mf-stats.json 或同类文件)。
    • 针对同一份共享依赖(例如 antd@6.1.0),汇总各应用声明的 usedExports 列表。
  2. 计算全局最小并集 合并并去重所有应用的 usedExports,得到该共享依赖在全局范围内必需模块的最小集合(并集)。

  3. 触发二次构建(生成优化共享包) 调用构建工具(如 Rspack CLI),以独立构建的方式传入关键信息:

    • 目标共享依赖的名称与版本(如 antd@6.1.0)。
    • 上一步得到的全局 usedExports 最小并集。 构建完成后,产出一个仅包含这些模块、体积更小的独立共享包。
  4. 更新并下发 Snapshot 将新生成的优化包上传到静态资源服务器(CDN)。然后,更新模块联邦的 Snapshot 文件(通常是远程的 mf-manifest.json),写入或更新以下关键字段:

    • secondarySharedTreeShakingEntry: 指向新生成的优化包的 URL。
    • secondarySharedTreeShakingName: 优化包的唯一名称。
    • treeShakingStatus: 标记为可用状态。 最后,将更新后的 Snapshot 文件下发给所有消费者应用。
  5. 运行时按需加载 当用户访问应用时,MF Runtime 会读取 Snapshot。如果检测到 treeShakingStatus 可用且满足加载条件,它会自动加载 secondarySharedTreeShakingEntry 指向的优化包,而不是全量包。

版本、缓存与兼容性建议

  • 严格版本管理是策略的基石。确保二次构建与运行时加载都基于精确版本号(如 antd@6.1.0),避免不匹配。
  • 为二次构建产物设置合理缓存策略。由于内容会随全局 usedExports 变化,建议使用基于内容哈希的命名,实现长期缓存。
  • 该流程天然向前兼容:未启用或不满足条件时仍加载全量包,不影响现有功能。

验证与回退

如何确认 Tree Shaking 生效?

  1. 网络面板检查:在浏览器开发者工具的“网络 (Network)”面板中,筛选加载的 JS 文件。确认加载的是二次构建生成的、带有特定标识(如 secondary 或哈希值)的优化包,而不是原始的全量包。其体积也应该显著小于全量包。
  2. 产物内容比对:直接下载并查看加载的共享依赖 JS 文件内容。搜索你未使用的组件或函数名(例如,你只用了 Button,可以去搜索 Modal),如果搜索不到,说明它们已被成功摇掉。
  3. 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

@module-federation/enhanced
@module-federation/rsbuild-plugin
Rspack built-in
import { TreeShakingSharedPlugin } from '@module-federation/enhanced';
// import { TreeShakingSharedPlugin } from '@module-federation/enhanced/rspack';
// ...
plugins: [
  new TreeShakingSharedPlugin({
    secondary: true,
    mfConfig,
  }),
];

Modern.js 项目

当你使用 @module-federation/modern-js-v3 插件时,可以在插件配置中设置 secondarySharedTreeShaking: true 来开启二次构建产物生成。

modern.config.ts
import mfPlugin from '@module-federation/modern-js-v3';
//...
plugins: [
  mfPlugin({
    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 只用到了 antdButton,加载了自己的树摇包。
  • 应用 B 用到了 antdModal,它会加载自己的全量包。

此时,页面上会同时存在两个不同版本的 antd 实例(一个极简版,一个完整版),这会破坏单例模式,可能导致样式冲突、状态不共享、甚至应用崩溃。

建议:对于必须保持单例的库,优先使用 server-calc 策略,确保所有消费者都使用同一个经过全局优化的共享包。如果只能使用 runtime-infer,请通过补充 usedExports 尽可能地让生成的包更完整,以降低冲突风险。

3. Tree Shaking 的命中条件是什么?

主要依赖于构建工具的静态分析能力。代码必须采用 ES Module (import/export) 语法,以便编译器能够分析出哪些导出被使用了。CommonJS (require/module.exports) 模块通常无法被有效 Tree Shaking。

4. 它是否会破坏已发布项目的共享依赖?

不会。 Tree Shaking 的数据源和加载路径与原有的共享机制是隔离的,并且有严格的命中条件和安全回退逻辑。它不会影响已经稳定运行的老项目。

延伸阅读