Umi 框架中的 MFSU 依赖预编译提速方案:原理、配置与实战排障

发布时间:2026/9/14 5:41:49
Umi 框架中的 MFSU 依赖预编译提速方案:原理、配置与实战排障 Umi 框架中的 MFSU 依赖预编译提速方案原理、配置与实战排障【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umiMFSUModule Federation Speed Up是 Umi 基于 webpack5 Module Federation 新特性实现的依赖预编译加速方案通过在开发期将变动较少的应用依赖预构建为 Module Federation remote 应用大幅缩短热更新等待时间。本文以 docs/docs/docs/guides/mfsu.en-US.md 为核心骨架结合packages/mfsu源码与 mfsu 配置文档系统讲解 MFSU 的两种分析策略、两种构建工具、完整配置项以及常见问题与解决方案帮助你在真实项目中选型、调优与排障。什么是 MFSUMFSUModule Federation Speed Up是一种基于 webpack5 新特性 Module Federation 的打包提速方案。其核心思路是分而治之divide and conquer将应用源代码的编译与应用依赖的编译分离把变动较小的应用依赖提前构建为一个 Module Federation 的 remote 应用从而在应用热更新时完全跳过对依赖的编译。开启 MFSU 可以大幅减少热更新所需的时间因此Umi 项目中默认开启 MFSU 功能。当然你也可以通过配置mfsu: false来关闭它。// 关闭 MFSU mfsu: false从源码实现看MFSU 的核心载体是packages/mfsu/src/mfsu/mfsu.ts中的MFSU类它负责在 webpack 配置中注入ModuleFederationPlugin将shared、remotes写入应用侧配置通过BuildDepPlugin在合适的时机触发依赖构建见packages/mfsu/src/webpackPlugins/buildDepPlugin.ts并通过getMiddlewares()提供mf-va_、mf-dep_、mf-static/等前缀的产物静态服务见 constants.ts 中的MF_VA_PREFIX、MF_DEP_PREFIX、MF_STATIC_PREFIX常量。MFSU 的两种工作策略MFSU 最关键的一点是如何把应用代码中实际使用到的依赖分析出来。根据分析方式的不同MFSU 有两种工作模式normal编译时分析与eager静态扫描。normal 策略编译时分析采用以下配置启用mfsu: { strategy: normal, }现代前端项目的代码都需要经过转译transpile才会在生产环境中使用。在转译过程中转译器比如 babel会在代码中插入新的依赖。这些插入的依赖在项目代码层面不可见但可以通过转译插件收集到。MFSU 编译时分析的工作方式为先对应用项目源码单独进行编译编译的同时收集项目本身的依赖和编译引入的依赖待项目代码编译完成以后再使用收集到的结果继续构建项目依赖部分的代码。整个过程是串行的——以 React 引用的构建为例必须先完成 React 的收集才能进入依赖构建阶段。在源码中normal 策略由 strategyCompileTime.ts 中的StrategyCompileTime实现它通过 babel 插件awaitImport见 babelPlugins/awaitImport/awaitImport.ts在转译过程中收集matched命中依赖与unMatched的模块引用并写入moduleGraph依赖构建的触发时机在BuildDepPlugin的onCompileDone回调中——即项目源码编译完成后才调用mfsu.buildDeps()这正是文档所说串行的代码级体现同时unMatchLibs会合并用户配置的unMatchLibs、shared中的依赖名以及remoteAliases这些库不会被收集进 MFSU 依赖而会随项目代码一起编译。eager 策略静态扫描mfsu: { strategy: eager, }与编译时分析不同扫描分析方式会先读取项目中的所有源代码文件然后通过静态分析获取项目依赖。这个过程非常快——在一个有 17 万行代码、1400 多个文件的项目中分析一次只需要 700ms 左右。如此快速分析的代价是收集到的依赖会缺失项目代码编译过程中插入的依赖这部分依赖最终会和项目代码一起编译打包。分析完项目依赖之后Umi 会拿着这份依赖信息并行地进行项目代码的编译和依赖的编译——从整体流程上看编译部分是并行的这正是 eager 策略冷启动更快的原因。在源码中eager 策略由 strategyStaticAnalyze.ts 中的StaticAnalyzeStrategy实现依赖收集基于StaticDepInfo见 staticDepInfo/staticDepInfo.ts它借助srcCodeCache提供的合并源码与importParser解析 import 语句并对babel-plugin-import这类库做运行时模拟见 staticDepInfo/simulations/babel-plugin-import.ts以保证import { Button } from antd这类按需引入写法能正确归因到antd依赖构建的触发时机在BuildDepPlugin的beforeCompile回调中——即在项目代码编译之前就启动buildDeps()与项目编译并行进行当源码文件发生变化新增、修改、删除 JS/JSX/TS/TSX 文件时会通过onFileChange回调增量更新依赖分析结果如果srcCodeCache未提供eager 策略会回退到 normal 策略fallback to MFSU normal strategy这一点在 mfsu.ts 构造函数中有明确处理。两种策略对比小结维度normal编译时分析eager静态扫描依赖收集方式babel 转译过程中收集读取源码静态解析依赖完整性完整含转译插入的依赖会遗漏转译插入的依赖随项目代码打包构建过程串行先项目代码再依赖并行项目代码与依赖同时构建冷启动速度相对较慢更快如 17 万行代码仅需约 700ms 分析适用场景monorepo、依赖相对稳定的项目大项目、依赖频繁变动的早期项目两种依赖构建工具MFSU 支持使用Webpack或esbuild构建项目的依赖。Webpack默认默认配置使用 Webpack和 Webpack 生态有很好的兼容性esbuild通过mfsu: { esbuild: true }开启享受 esbuild 的高效构建速度。在源码中两种构建方式由 depBuilder.ts 中的DepBuilder统一调度buildWithWebpack会基于getWebpackConfig生成依赖构建配置注入ModuleFederationPlugin将每个依赖通过exposes暴露filename为remoteEntry.js、DepChunkIdPrefixPlugin、StripSourceMapUrlPlugin并通过splitChunks将依赖合并进mf-dep_前缀的 vendor chunkbuildWithESBuild通过getESBuildEntry生成入口内容后调用umijs/bundler-esbuild的build产出mf-va_remoteEntry.js在 eager 策略下依赖构建还可以通过buildWithWorker放入 worker 线程执行避免阻塞主构建进程。mfsu 完整配置说明mfsu的完整配置类型与默认值如下参见 config.md 的 mfsu 章节// 类型 { esbuild: boolean; mfName: string; cacheDirectory: string; strategy: normal | eager; include?: string[]; chainWebpack: (memo, args) void; exclude?: Arraystring | RegExp; } // 默认值 { mfName: mf, strategy: normal }各参数说明参数说明esbuild置为true后依赖的预编译走 esbuild首次启动更快缺点是二次编译没有物理缓存稍慢。推荐项目依赖比较稳定的项目使用mfName该方案 remote 库的全局变量名默认mf。通常在微前端中为避免主应用与子应用冲突才需要配置cacheDirectory自定义缓存目录默认node_modules/.cache/mfsuchainWebpack用链式编程方式修改依赖的 webpack 配置基于 webpack-chainruntimePublicPath让修改 mf 加载文件的 publicPath 为window.publicPathstrategy指定依赖编译时机normal为 babel 编译分析后构建 Module Federation 远端包eager为静态分析后与项目代码同时发起构建include仅在strategy: eager模式下生效用于补偿静态分析无法分析到的依赖。例如react未进入 Module Federation 远端模块时可配置{ include: [react] }exclude手动排除不需要被 MFSU 处理的依赖支持字符串或正则。如{ exclude: [vant] }为全词匹配{ exclude: [/vant/] }表示只要 import 路径命中该正则的依赖都不走 MFSU配置示例// 用 esbuild 做依赖预编译 mfsu: { esbuild: true, } // 关闭 mfsu 功能 mfsu: false;// webpack 配置修改 mfsu: { chainWebpack(memo, args) { // 添加额外插件 memo.plugin(hello).use(Plugin, [...args]); return memo; } }从源码看MFSU 的缓存文件会写入依赖构建目录eager 策略的依赖信息缓存在.mfsu/MFSU_CACHE_v4.json见 staticDepInfo.ts 中的cacheFilePath定义而依赖产物目录默认是node_modules/.cache/mfsushouldBuild()会基于缓存快照判断依赖是否需要重新构建见 mfsu.ts 的 buildDeps 方法命中缓存时直接skip buildDeps。如何选择策略与构建工具的选型建议基于上文对优缺点的分析文档给出以下建议如果不使用 Module Federation 的功能且项目依赖变动不频繁建议先尝试 esbuild 构建如果在 monorepo 项目中推荐使用normal策略并推荐开启配置 monoreporedirect如果你的项目较大、项目代码基数较大推荐使用eager策略如果项目刚刚启动、会频繁改动依赖推荐使用eager策略其他类型的项目则可以随意选择。核心决策逻辑可以归纳为normal编译时分析优点是收集的依赖完整项目代码和依赖代码的构建打包完全分离修改项目代码后只需构建项目代码部分缺点是构建过程串行eager扫描方式优点是耗时的代码构建全部并行对较大项目的冷启动时间改善非常明显缺点是一部分运行时依赖会和项目代码一起编译。常见问题与解决方案依赖缺失在 eager 策略下可能出现如下报错error - [MFSU][eager] build worker failed AssertionError [ERR_ASSERTION]: filePath not found of lodash.capitalize解决方式检查你的依赖确保对应的依赖已经安装如例子中的lodash.capitalize。React 多实例问题在浏览器中会出现如下报错根因是在某些复杂场景下React 的代码被打包多份在运行时产出了多个 React 实例。解法是通过 Module Federation 的shared配置来避免多实例的出现mfsu: { shared: { react: { singleton: true, }, }, },如果有其他依赖出现多实例问题也可以通过类似方式解决。⚠️ 注意如果开启了 MF 插件需要开启shared。在 Umi Max 的 MF 插件场景下插件会自动为 MFSU 填充兼容的默认配置——包括remoteName、remoteAliases以及react/react-dom的singleton: true, eager: true的shared配置从而保证开启 MFSU 后也能在 DEV 阶段调试 MF 模块详见 max/mf.md 的和 MFSU 一起使用章节。externals script 兼容问题如果项目依赖 aa 依赖 b而项目配置了 b 的 script 类型的 externals 如下externals: { b: [script https://cdn/b.js, b] }在开启 MFSU 时会报错。例如以下代码import * as b from b; console.log(b);上述代码中的 b 正常应该是 Module 信息拿到的却是PromiseModule。这是 webpack 自身的问题——没有处理好 externals script 和 module federation 之间的兼容问题可能也因为 externals script 很少有人知道和使用。解法是不要和 MFSU 混着用只在process.env.NODE_ENV production时开启externals: { ...(process.env.NODE_ENV production ? {b: [script https://cdn/b.js, b]} : {}) }依赖环问题场景 1monorepo 中的依赖环在使用 monorepo 时项目依赖了 A 包A 依赖了 B 包而 B 包是项目 monorepo 中子包提供的这就形成了项目源码依赖 AA 又重新依赖项目源码的情况。这种情况建议使用 MFSU 的exclude配置mfsu: { exclude: [ B ] }场景 2依赖内部不合理实现导致的编译失败如果项目的某个依赖存在不合理的实现例如依赖了 Bigfish 插件相关的功能即引用了.umi目录下的内容在开启 MFSU 后项目可能无法正常编译。解法与场景 1 类似将这个包配置进mfsu.exclude字段中。worker 兼容问题如果项目代码需要在 Worker 中使用那么需要将 Worker 需要的依赖添加到 MFSU 的 exclude 配置 中。Worker 相关依赖只能通过这种方式绕过因为 Module Federation 是通过window对象来共享模块的所以在 worker 中不能使用 Module Federation 中的模块。深入源码MFSU 的运行时产物与请求链路理解 MFSU 的产物组织方式有助于排查问题。在 constants.ts 中定义了几个关键前缀常量MF_VA_PREFIX mf-va_虚拟入口产物前缀远端入口文件为mf-va_remoteEntry.js即REMOTE_FILE_FULLMF_DEP_PREFIX mf-dep_依赖 chunk 前缀getWebpackConfig中的splitChunks.cacheGroups.vendor会把所有依赖合并进该前缀的 chunkMF_STATIC_PREFIX mf-static/静态资源前缀DEFAULT_MF_NAME mfremote 库默认全局变量名DEFAULT_TMP_DIR_NAME .mfsu默认临时产物目录。在 mfsu.ts 的 getMiddlewares() 方法 中开发服务器会对以这些前缀开头的请求做拦截等待依赖构建完成后直接从临时目录读取对应文件返回并为非remoteEntry.js的产物设置cache-control: max-age31536000,immutable一年强缓存保证依赖产物在热更新阶段只需加载一次。应用侧入口则通过WebpackVirtualModules生成mfsu-virtual-entry下的虚拟入口将各个 entry 改写为await import(...)的动态加载形式再通过ModuleFederationPlugin的remotes配置指向 MFSU 构建出的 remote 应用mfsu.ts 的 setWebpackConfig 方法。结语MFSU 通过分而治之的思路把依赖编译从项目热更新链路中剥离是 Umi 默认开启的提速利器。理解normal与eager两种策略的取舍、Webpack 与 esbuild 两种构建工具的特点并结合include、exclude、shared等配置解决多实例、依赖环等边界问题就能在真实项目中把 MFSU 的性能收益发挥到最大。进一步阅读可以查看 packages/mfsu/src 的源码与测试用例或参考 Umi 官方文档中的 mfsu 配置章节 与 MF 插件文档。【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考