Skip to content

预加载与性能代价

基于微前端 2026 生态 · 核于 2026-07

速查

  • 微前端把「页面切换」变成「应用切换」:下载 HTML/JS/CSS → 沙箱初始化 → 执行 → mount 全流程——预加载的任务是把这条链搬到用户点击之前
  • qiankun prefetch 四形态true首个子应用挂载完成后预拉其他应用资源)/ 'all'(主应用 start 时就全量预拉)/ string[](首个挂载后只预拉指定应用)/ function(自定义,返回 { criticalAppNames, minorAppsName } 分两级)
  • 预加载 ≠ 预执行:prefetch 只是把资源拉进 HTTP 缓存,JS 解析执行、渲染仍留在切换时——首跳仍有可感延迟
  • wujie 分级更激进preloadApp 提前实例化 iframe + 缓存资源;**预执行(exec)**在预加载阶段直接运行子应用代码,达到「类 SSR 的打开体验」;空闲调度用 requestIdleCallback
  • wujie 保活(keep-alive):切走不销毁 iframe 与容器,状态全留、再进秒开——内存换时间,保活应用数量必须设上限
  • 预执行与保活答的是两个问题:预执行优化首访(第一次进就快)、保活优化回访(再进秒开且状态还在)——别拿保活当首访优化用
  • MF 的隐性预加载shareStrategy: 'version-first'(默认)为版本协商在初始化即拉全部 remote 入口——远端离线启动就炸;loaded-first 按需加载、容错好
  • 性能代价四连:重复运行时(Fowler:每个微前端带一份 React = 用户下载 n 次)、公共依赖抽取 → 构建耦合回潮(协同升级战役)、请求瀑布(MF 2.0 公告点名的微前端固有问题)、沙箱运行时税(with/Proxy 查找、样式改写)
  • 请求瀑布的形态:HTML entry 链 = fetch HTML → 解析 → fetch JS/CSS → 执行 → mount;MF 链 = manifest → remoteEntry → 共享协商 → 业务 chunk——每级都是一轮 RTT
  • 瀑布缓解四思路:清单前移(manifest/import map 让第一跳知道整条链)、层内并行(按清单批量拉)、时间搬运(预加载挪到空闲期)、架构级(SSR/Data Prefetch,MF 2.0 规划)
  • Fowler 实测立场:独立编译「等效于实现了我们自己的 code splitting」——每页只下载所需,单页可能反而比单体更快;「想确知性能影响,没有什么能替代真实环境的测量,最好在生产环境」
  • 微前端特有账目要单列观测:首跳时间、切换时间、依赖副本数(共享静默失效的信号)、启动期请求串行深度、内存水位(保活成本)
  • 治理组合拳:预加载分级(关键应用 › 次要应用 › 不预拉)+ 共享白名单小而稳 + keep-alive 设上限 + 性能预算与真实监控兜底

一、为什么预加载是微前端的刚需

单体 SPA 里「切页面」是路由组件的挂载,代价以毫秒计;微前端里「切应用」是一整条冷启动链:

text
用户点击 → 下载子应用 HTML → 解析出 JS/CSS 清单 → 逐个下载
        → 沙箱初始化 → 脚本执行 → bootstrap → mount → 可交互

每一环都在用户的等待里。预加载的本质是把这条链的前半段搬到点击之前——用主应用空闲期的带宽,换子应用切换时的体感。但「搬多少、什么时候搬」是门权衡艺术:搬得太少没效果,搬得太多挤占主应用首屏、白白消耗流量与内存。各框架的预加载 API 本质都是在回答同一个调度问题,只是激进程度不同。

二、qiankun prefetch:四形态的资源预拉

qiankun 把预加载做成 start({ prefetch }) 的一个配置项,四种取值对应四种调度策略(API 一手语义):

取值触发时机预拉范围适用
true(默认)首个子应用 mount 完成后其他所有已注册应用的静态资源通用默认:先保首跳,再填后路
'all'主应用 start()全部已注册应用应用少、内网带宽富裕的中后台
string[]首个子应用 mount 完成后数组指定的应用明确知道用户下一步去哪
function自定义返回 { criticalAppNames, minorAppsName } 两级名单精细分级:关键应用先拉、次要应用缓拉

三个设计细节值得注意:

  • true'all' 的差异是跟主应用抢不抢首屏true 刻意等到首个应用挂载完(首屏已成)才动手;'all'start() 时就开闸,换取后续切换全命中缓存;
  • function 形态的两级名单暴露了预加载的正确心智——应用是有优先级的:「关键应用(criticalAppNames)尽快拉、次要应用(minorAppsName)缓拉」,而不是一视同仁;
  • 预拉执行在浏览器空闲期requestIdleCallback 型调度思想,wujie 的分级加载同款)——不与用户当前交互抢主线程与带宽。

局限也要说透:prefetch 预拉的是资源(进 HTTP 缓存),不做解析执行,更不做挂载。切换时省掉的是网络时间,JS 的 parse/execute、框架初始化、沙箱构建仍在点击之后——对重型子应用,首跳依然可感。要吃掉这段时间,就得往下一节的「预执行」走。

三、wujie 分级:预加载、预执行与保活

wujie 把「提前做多少」拆成了一条完整的滑竿,三档能量逐级升高(一手文档语义):

  1. 预加载(preloadApp:提前创建子应用的 iframe 实例并缓存静态资源——比纯资源预拉多做了「沙箱实例化」这步;调度上可结合 requestIdleCallback,只吃浏览器空闲时间。
  2. 预执行(exec:在预加载基础上直接执行子应用代码——组件树在用户点击前就已渲染在内存容器里,官方称达到「类 SSR 的打开体验」。这是预加载谱系里最激进的一档:切换时几乎只剩「把已渲染的容器亮出来」。
  3. 保活(keep-alive):针对回访的优化——切走时不销毁 iframe 与 WebComponent 容器,子应用状态(表单、滚动位置、组件树)原样保留,再次进入无需重新渲染,类比 Vue 的 keep-alive

这条滑竿的另一头是账单。预执行把子应用的执行成本提前到主应用运行期——抢的是主线程与内存,预执行的应用多了,主应用自己先卡;保活则是纯粹的内存换时间——每个保活应用都是一个常驻 iframe + 完整组件树,不设上限的保活等于自己造内存泄漏。

三档怎么发牌,判据可以列成一张决策表:

判据什么都不给预加载预执行保活
访问概率长尾(<10%)中等高(用户大概率下一步)高频往返
应用体积大也无妨(反正不拉)越大越值得预拉大应用收益最大、代价也最大与体积无关,与状态有关
状态保持需求表单/长列表/多步流程必选
预支的资源带宽带宽 + 主线程 + 内存常驻内存
典型应用设置页、帮助中心二级业务模块首页旁的核心工作台高频切换的核心双子应用

四、MF 的隐性加载策略

Module Federation 场景没有「子应用切换」,但共享协商引入了自己的加载时机问题,常被忽略。依赖共享里讲过 shareStrategy 的语义,这里从性能视角再看一遍:

  • version-first(默认):为了保证「singleton 最高版本获胜」裁决准确,运行时在初始化阶段就把所有 remote 的入口文件拉一遍登记版本——这是一次隐性的全量预加载。收益是版本裁决全局最优;代价是启动期网络放大,且 MF 文档的 warning 写得很直白:任何 remote 离线,启动期就触发 beforeLoadShare 阶段的失败,没有错误兜底就是初始化挂起或白屏。
  • loaded-first:remote 按需加载、优先复用已加载副本——启动轻、离线只影响真正被访问的模块,代价是版本裁决可能非全局最优。

MF 2.0 的 mf-manifest.json 协议(含 remoteEntry、shared、exposes、chunks 等元信息)则把预加载往平台层推:部署平台读 manifest 即可分析模块依赖关系,做精确的预加载与灰度——公告原文称这份信息是「分析项目间依赖、构建与优化平台的基石」。

五、性能代价清单:拆分不是免费的

预加载解决「什么时候付钱」,这一节列「总共要付多少」。微前端相对单体的四笔结构性开销:

1. 重复运行时下载。 Fowler 的原话最直接:「如果每个微前端都带一份自己的 React,就是在强迫用户下载 n 次 React」。这是不做依赖共享时的默认账单——注意它按字节计费,且随子应用数量线性增长。

2. 公共依赖抽取 → 构建耦合回潮。 为了消掉第 1 笔账去抽公共依赖,Fowler 的警告立刻生效:「重新引入了构建时耦合……可能落得一场大规模协同升级」。这笔账不按字节计费,按组织成本计费——共享名单里每个包的 major 升级,都要全体子应用排期。第 1、2 笔是跷跷板,依赖共享三路线整页都在讲怎么在两头之间落座。

3. 请求瀑布(request waterfall)。 运行时集成把「构建期就能确定的依赖关系」推迟到运行时逐级发现:

text
HTML entry 链:fetch HTML ──► 解析清单 ──► fetch JS/CSS ──► 执行 ──► mount
MF 链:       fetch manifest ──► fetch remoteEntry ──► 共享协商 ──► fetch 业务 chunk

每一级都要等上一级返回才知道下一步拉什么——每级一轮 RTT,弱网下逐级放大。这不是某个框架的实现瑕疵:MF 2.0 公告原文点名「Module Federation 同样面临微前端架构固有的『请求瀑布问题』」,并把 SSR 与 Data Prefetch 列入规划。缓解思路都围绕同一句话——让第一跳就知道整条链

  • 清单前移:mf-manifest / import map 把资源关系提前写成静态清单,加载器一次读全、并行去拉;
  • 层内并行:HTML entry 解析出清单后,全部样式与脚本并行拉取(import-html-entry 的 getExternalScripts/getExternalStyleSheets 即按清单批量取);
  • 时间搬运:本页前半部分的预加载——瀑布还在,但流在用户看不见的空闲期;
  • 架构级方案:SSR 与数据预取(MF 2.0 规划方向),把瀑布挪到服务端内网去流。

4. 沙箱运行时税。 隔离不是免费的:with + Proxy 沙箱给每次变量查找加一层陷阱调用、样式改写给每条规则加一次解析重写、iframe 路线按应用数付内存(各自的量化讨论见 JS 沙箱谱系CSS 隔离)。这笔税在应用切换与高频交互路径上最可感。

六、Fowler 实测原则与治理建议

把四笔账摆完,很容易滑向「微前端 = 性能灾难」的结论——Fowler 的实测立场恰好是解毒剂。他指出独立编译有个常被忽略的反向红利:「通过独立编译每个页面,我们等效于实现了自己的 code splitting——任何单页加载只会下载该页的源码与依赖」,因此「即使对重复依赖什么都不做,单个页面仍可能比单体前端加载得更快」。单体应用的 bundle 里塞着所有页面的代码,微前端天然按页切割——两边的账不能只算一头。

他的方法论结论更值得抄在墙上:「想确知某个变化的性能影响,没有什么能替代真实环境的测量,最好是在生产环境」——payload 的「可能」与「未必」都不该拍脑袋裁决。落到治理动作上:

  1. 预加载分级:照 qiankun function 形态 / wujie 三档的思路,把应用分成「关键 / 大概率 / 长尾」三级,分别给预执行(或保活)、预加载、什么都不给——全量 'all' 与全不预拉都是偷懒;
  2. 共享白名单小而稳:只共享大且稳定的库(React/Vue 级别),小库各自带走(single-spa 立场),每加一项共享都记一笔构建耦合的账;
  3. keep-alive 与预执行设上限:内存与主线程是预支出来的,给保活应用数量设硬上限,预执行只给「点击概率过半」的应用;
  4. 预算 + 真实监控:给关键指标设预算并用真实用户监控(RUM)验证——通用性能手段(code splitting、CDN、缓存策略)归前端优化章,这里只强调微前端特有的账目要单列观测
观测项衡量什么恶化信号
首跳时间(首个子应用可交互)主应用启动 + 首应用整条冷启动链prefetch: 'all' 抢首屏、eager 共享撑大入口
应用切换时间预加载策略的直接成绩单预拉名单与真实动线不符(拉了不去、去了没拉)
页面总下载字节 / 依赖副本数重复运行时与共享失效bundle 里出现第二份 React(共享静默失效)
启动期请求数与串行深度请求瀑布 + version-first 全量拉取remote 数量增长后启动线性变慢
内存水位keep-alive 与预执行的常驻成本保活应用数不设上限、切换后内存不回落

小结

预加载与性能是同一枚硬币:微前端把应用启动链搬进了运行时,预加载的全部技巧就是把这条链再搬回用户点击之前——qiankun prefetch 四形态管「资源何时拉」(默认首应用挂载后、'all' 提前到 start、function 分两级),wujie 三档管「提前做到哪一步」(实例化 → 预执行 → 保活),MF 的 version-first 则是一次为版本协商服务的隐性全量预拉(离线 remote 是它的阿喀琉斯之踵)。代价侧背下四笔账:重复运行时、共享带来的构建耦合、逐级发现依赖的请求瀑布(MF 2.0 公告点名)、沙箱运行时税。而 Fowler 的两句话负责校准心态:独立编译天然自带按页 code-split,单页未必更慢;性能结论只能来自真实测量,不能来自直觉。至此四大机制 + 加载性能全部讲完,参考页备好了全部速查表。