预加载与性能代价
基于微前端 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 里「切页面」是路由组件的挂载,代价以毫秒计;微前端里「切应用」是一整条冷启动链:
用户点击 → 下载子应用 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 把「提前做多少」拆成了一条完整的滑竿,三档能量逐级升高(一手文档语义):
- 预加载(
preloadApp):提前创建子应用的 iframe 实例并缓存静态资源——比纯资源预拉多做了「沙箱实例化」这步;调度上可结合requestIdleCallback,只吃浏览器空闲时间。 - 预执行(
exec):在预加载基础上直接执行子应用代码——组件树在用户点击前就已渲染在内存容器里,官方称达到「类 SSR 的打开体验」。这是预加载谱系里最激进的一档:切换时几乎只剩「把已渲染的容器亮出来」。 - 保活(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)。 运行时集成把「构建期就能确定的依赖关系」推迟到运行时逐级发现:
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 的「可能」与「未必」都不该拍脑袋裁决。落到治理动作上:
- 预加载分级:照 qiankun function 形态 / wujie 三档的思路,把应用分成「关键 / 大概率 / 长尾」三级,分别给预执行(或保活)、预加载、什么都不给——全量
'all'与全不预拉都是偷懒; - 共享白名单小而稳:只共享大且稳定的库(React/Vue 级别),小库各自带走(single-spa 立场),每加一项共享都记一笔构建耦合的账;
- keep-alive 与预执行设上限:内存与主线程是预支出来的,给保活应用数量设硬上限,预执行只给「点击概率过半」的应用;
- 预算 + 真实监控:给关键指标设预算并用真实用户监控(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,单页未必更慢;性能结论只能来自真实测量,不能来自直觉。至此四大机制 + 加载性能全部讲完,参考页备好了全部速查表。