
MFSU 为何比 Vite 更快Umi 4 依赖预构建提速方案全解析【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umiUmi 4 同时内置了 Webpack 与 Vite 两种构建方式而 MFSUModule Federation Speed Up正是 Umi 团队为了让 Webpack 生态既保留完整功能、又能获得接近甚至超越 Vite 的开发体验而给出的答案。本文以 Umi 官方博客《比 Vite 还快的 MFSU》为骨架结合本仓库中umijs/mfsu的源码实现与配置文档完整还原那场两个示例、四种模式、四个维度的对比实验并深入拆解 MFSU 的原理、策略、构建工具与全部配置项帮助你在自己的项目中正确开启并调优 MFSU。MFSU 是什么分而治之的打包提速方案MFSU 是一种基于 Webpack 5 新特性Module Federation的打包提速方案。其核心思路是分而治之将应用源代码的编译与应用依赖的编译分离把变动较小的第三方依赖提前构建为一个 Module Federation 的 remote 应用。这样一来日常开发中修改源码触发热更新时Webpack 不再需要重复编译庞大的node_modules依赖热更新耗时被大幅压缩。用原博客编者按的话说Change the code, dont Workaround!—— Webpack 慢就去改它优化到位后Bundle 同样可以很快。该方案在 Umi 4 中默认开启适用于既要 Webpack 功能与生态又想要 Vite 速度的场景。从本仓库源码可以印证这一点在 preset-umi 的配置默认值 中mfsu的默认配置被设置为{ strategy: eager }官方文档 MFSU 指南 也明确说明我们在 Umi 的项目中默认开启了 MFSU 功能如需关闭只需配置mfsu: false。为什么要做这场对比Umi 4 的 Webpack / Vite 双轨制Umi 4 同时支持 Webpack 和 Vite 两种构建方式跑通之后Umi 团队迫不及待地对Vite与Webpack MFSU的效果进行了对比结果有点意外。原博客给出了一个非常工整的实验设计两个示例大型的全量 ant-design-pro 和小型的 libs example四种模式webpack、webpack MFSU、webpack MFSU with esbuild、Vite in umi四个维度无缓存的冷启动、有缓存的热启动、修改代码后的热更新、页面打开速度。这一对比结构清晰地回答了开发者最关心的问题换构建工具之前能不能先用依赖预构建把 Webpack 的速度拉起来统计口径与公平性说明为了让数据尽量可信原博客对统计口径做了详细交代上述 Webpack 相关模式全部开启物理缓存持久化缓存Vite 是 Umi 中集成后的 Vite经 Vite 开发者确认基本排除误用的可能性大段时间消耗在预编译依赖上Ant Design Pro 中包含 less 的使用而 less 是 esbuild无法加速的部分该因素对各模式是公平的数据取自13-inch M1 2022 版 Mac重启电脑后连续跑5 次取平均值Vite 的热更速度没有统计因为受 ESM 特性限制改完后要等请求过来处理完才算结束无法统一统计但肯定是很快的。如何手动验证如果你有兴趣手动验证可以拉取本仓库git clone后进入不同的 example 目录执行npm run dev进行复测。仓库中与原文实验直接对应的目录包括大型示例examples/ant-design-pro小型示例examples/libs另有专门用于 MFSU 端到端验证的 examples/mfsu-e2e包含 cypress 用例可自动化观测构建行为对比结论MFSU with esbuild 数据领先原博客的结论可以概括为两点在所列场景下MFSU with esbuild 模式的数据全面领先冷启动、热启动、热更新三个维度四种模式的页面打开速度差不多因此对比图中没有列出该维度的数据。这出乎作者的意料——原以为 Vite 请求多会让页面打开速度变慢结果差异不大也可能与示例项目复杂度有关。值得注意的是原博客的对比结论基于作者当时的实测环境M1 2022、特定示例与依赖版本具体数值见原文配图本文不再赘述具体毫秒数读者应以自己项目的实测为准。为什么 MFSU 能快从原理层面解读对比结果看似Webpack 逆袭 Vite实则并不神秘冷启动MFSU 通过 Module Federation 把依赖预构建成 remote应用代码构建与依赖构建解耦尤其eager策略下两者可并行进行详见下文将 Webpack 冷启动的串行瓶颈打破。热启动依赖构建结果落盘缓存默认node_modules/.cache/mfsu再次启动时直接跳过依赖编译。热更新源码变更时依赖 bundle 完全不变Webpack 增量编译只针对项目代码HMR 开销大幅下降。这与 Vite 的依赖预构建esbuild optimize deps ESM 按需加载在思路上异曲同工只是 MFSU 把预构建这件事建立在 Webpack 的 Module Federation 机制之上从而完整保留了 Webpack 的 loader 生态与配置能力。MFSU 的两种策略normal 与 eagerMFSU 最关键的环节是如何准确分析出应用代码实际使用到的依赖。根据分析方式的不同MFSU 有两种工作策略见 MFSU 指南 与 config 文档。normal 策略编译时分析mfsu: { strategy: normal, }现代前端项目的代码必须经过转译transpile才能在生产环境运行。在转译过程中转译器如 Babel会在代码中插入新的依赖这些依赖在项目源码层面不可见只能通过转译插件收集。normal 策略的工作方式是先对应用源码单独编译编译的同时收集项目自身依赖 编译期插入的依赖待项目代码编译完成后再用收集结果去构建依赖部分的代码。整个过程是串行的以 React 引用为例先分析、再构建依赖、最后编译应用。对应到源码normal 策略由 StrategyCompileTime 实现它基于 Babel 插件awaitImport在编译期收集 import 信息通过moduleGraph.onFileChange记录每个文件的依赖关系与版本Dep.getDepVersion并支持unMatchLibs、remoteAliases、shared等豁免规则。eager 策略静态扫描mfsu: { strategy: eager, }与编译时分析不同扫描方式会先读取项目中所有源代码文件通过静态分析获取依赖。这个过程非常快——原博客与指南给出参考在一个 17 万行代码、1400 多个文件的项目中分析一次只需约 700ms。如此快速的分析也有代价收集到的依赖会缺失编译期插入的依赖这部分漏网依赖最终会和项目代码一起编译打包。分析完成后Umi 会拿着依赖信息并行去构建项目代码和依赖代码编译过程不再是串行的这正是 eager 策略对大型项目冷启动改善明显的原因。对应源码eager 策略由 StaticAnalyzeStrategy 实现它借助StaticDepInfosrcCodeCache对源码做静态 import 解析并在 webpack 的beforeCompile钩子中触发依赖构建、在onFileChange钩子中增量分析变化的 JS 文件通过正则\.(jsx|js|ts|tsx)$过滤。如何选择基于两种策略的优缺点官方指南给出如下建议不使用 Module Federation、依赖变动不频繁的项目先尝试 esbuild 构建monorepo 项目推荐normal 策略并建议开启monoreporedirect配置项目较大、代码基数大推荐eager 策略项目刚启动、频繁改动依赖推荐eager 策略其他项目可随意选择。另外需要留意虽然 schema 中strategy的默认值是normal见 bundler-webpack schema但 Umi 的 preset 在实际项目中通过configDefaults将默认策略覆盖为eager见 configPlugins.ts这也是本仓库当前版本的实际默认行为。两种依赖构建工具Webpack 与 esbuildMFSU 支持用Webpack或esbuild来构建依赖 bundleWebpack默认与 Webpack 生态兼容性最好esbuild通过mfsu.esbuild: true开启享受 esbuild 的高效构建速度。// 用 esbuild 做依赖预编译 mfsu: { esbuild: true, }这正是原博客对比中webpack MFSU with esbuild模式的数据来源。从源码看DepBuilder 的build方法会根据mfsu.opts.buildDepWithESBuild决定走buildWithESBuild调用umijs/bundler-esbuild的build产物直接写入tmpBase还是buildWithWebpack生成一个包含 ModuleFederationPlugin 的 webpack 配置将依赖合并进 vendor chunk 并输出 remoteEntry。在 esbuild 路径下构建完成会打印[mfsu] compiled with esbuild successfully in xx ms便于观察耗时。从源码看 MFSU 的实现机制核心类MFSUpackages/mfsu/src/mfsu/mfsu.ts 定义了MFSU类它是整个方案的调度中心职责包括入口改造通过WebpackVirtualModules把应用入口包装为虚拟模块将入口中的具名导出转为await import(...)的动态引入避免把依赖打进应用 chunk见setWebpackConfig接入 Module Federation注入ModuleFederationPlugin以mfName默认mf作为 remote 名称remoteEntry的加载地址支持静态 publicPath 或基于window.publicPath的 Promise 动态 remote启动依赖构建buildDeps()判断shouldBuild()后调用DepBuilder.build()支持useWorker参数决定是否在独立线程构建构建完成后写入缓存strategy.writeCache()提供服务中间件getMiddlewares()拦截mf-va_、mf-dep_等前缀的请求从tmpBase读取构建产物并返回其中非 remoteEntry 资源设置cache-control: max-age31536000,immutable的强缓存暴露编译插件getBabelPlugins()/getEsbuildLoaderHandler()返回对应策略的 import 改写插件。依赖收集的结果由Dep与DepModule承载Dep.buildDeps()负责把静态依赖列表转换为可构建的依赖对象并从 package.json 读取版本信息用于缓存失效判断。独立线程构建depBuildWorker在 eager 策略下依赖构建可以在worker 线程中进行从而与应用代码编译并行。见 packages/preset-umi/src/commands/dev/depBuildWorker/depBuildWorker.ts文件顶部直接断言isMainThread false确保只能运行在 worker 线程worker 内部复用 dev 配置getDevConfig/getConfig因为启动一个 Service 的成本比较高2-3 秒所以通过parentPort消息驱动构建复用同一个 Service主线程通过startBuildWorker(deps)发送构建请求worker 通过{ done: { withError } }回传结果进度通过progress消息上报。这正是 eager 策略项目代码编译与依赖构建并行的底层支撑。mfsu 配置参数全解根据 config 文档 与 schema 定义mfsu支持以下参数参数类型说明esbuildboolean置为true后依赖预编译走 esbuild首次启动更快缺点是二次编译没有物理缓存稍慢一些推荐依赖稳定的项目使用mfNamestring方案的 remote 库全局变量名默认mf通常在微前端中为避免主应用与子应用冲突而配置cacheDirectorystring自定义缓存目录默认node_modules/.cache/mfsuchainWebpack(memo, args) void用链式编程基于 webpack-chain修改依赖的 webpack 配置strategynormal \| eager依赖编译时机normal用 babel 编译分析后构建 Module Federation 远端包eager用静态分析与项目代码同时发起构建includestring[]仅在eager模式下生效用于补偿静态分析不到的依赖例如{ include: [react] }excludeArraystring \| RegExp手动排除不走 MFSU 的依赖字符串为全词匹配如{ exclude: [vant] }正则则匹配 import 路径如{ exclude: [/vant/] }runtimePublicPathboolean让 mf 加载文件的 publicPath 改为window.publicPathremoteAliasesstring[]与remoteName配合的远端别名豁免规则源码中会转成正则参与匹配remoteNamestring项目导出的远端模块名入口处理时会跳过该 key避免被动态加载改写sharedRecordstring, anyModule Federation 的shared配置用于控制依赖共享与单实例官方给出的典型配置示例// 关闭 mfsu 功能 mfsu: false; // 用 esbuild 做依赖预编译 mfsu: { esbuild: true, } // 修改依赖的 webpack 配置 mfsu: { chainWebpack(memo, args) { memo.plugin(hello).use(Plugin, [...args]); return memo; } } // 解决 React 多实例 mfsu: { shared: { react: { singleton: true, }, }, }注意mfsu功能默认开启配置mfsu: false才可关闭。常见问题与排查指南依赖缺失eager 模式下如果依赖没有正确安装构建会报错error - [MFSU][eager] build worker failed AssertionError [ERR_ASSERTION]: filePath not found of lodash.capitalize解法检查对应的依赖如lodash.capitalize是否已安装。React 多实例问题浏览器中如果出现 React 多实例报错根因是某些复杂场景下 React 被重复打包、运行时产生了多个 React 实例。解法是通过 Module Federation 的shared配置强制单实例mfsu: { shared: { react: { singleton: true, }, }, },其他依赖出现多实例时可用类似方式解决。注意如果开启了 MF 插件max 的umijs/maxMF 能力需要同步开启shared两者需配合使用。externals script 兼容问题如果项目依赖 aa 依赖 b而项目用 script 类型的 externals 配置了 bexternals: { b: [script https://cdn/b.js, b] }开启 MFSU 后import * as b from b拿到的会是PromiseModule而非模块对象这是 Webpack 在 externals script 与 Module Federation 之间的兼容问题。解法是避免与 MFSU 混用仅在process.env.NODE_ENV production时开启externals: { ...(process.env.NODE_ENV production ? { b: [script https://cdn/b.js, b] } : {}) }依赖环问题场景 1monorepo项目依赖 AA 依赖 B而 B 由项目 monorepo 子包提供形成源码 → A → 源码的环。建议用exclude配置将 B 排除mfsu: { exclude: [B] }场景 2依赖引用 .umi 内部内容某依赖不合理的引用了.umi目录下的内容如 Bigfish 插件相关功能开启 MFSU 后可能无法编译。解法同上将该包配置进mfsu.exclude。worker 兼容问题如果项目代码需要在 Web Worker 中使用某依赖需将该依赖加入mfsu.exclude。因为 Module Federation 是通过window对象共享模块的worker 线程中无法使用 MF 模块只能通过排除来绕过。小结回到那场对比实验在冷启动、热启动、热更新三个维度上webpack MFSU with esbuild取得了领先而页面打开速度与 Vite 相当。这背后不是玄学而是依赖预构建 源码/依赖分离 并行构建的组合拳用Module Federation把依赖做成 remote热更新只编译源码用eager 静态扫描把冷启动的构建过程并行化用esbuild加速依赖本体的编译用worker 线程 物理缓存进一步摊薄每一次启动的开销。如果你正在 Webpack 生态中挣扎于启动与热更新速度又不想为了速度迁移 ViteMFSU 提供了第三条路保留 Webpack 的能力拿走 Webpack 的慢。更多细节可继续阅读 MFSU 指南、mfsu 配置文档或深入 packages/mfsu 源码研究其实现。【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考