微前端子应用加载性能:预加载、预取与懒加载的带宽博弈

发布时间:2026/7/12 15:48:36
微前端子应用加载性能:预加载、预取与懒加载的带宽博弈 微前端子应用加载性能预加载、预取与懒加载的带宽博弈一、首屏 18 秒9 个子应用都在等各自的 bundle微前端架构落地后一个典型的首页场景是主应用 导航栏子应用 工作台子应用 通知子应用……9 个子应用的 JS Bundle 共有 4.2MB首屏全部加载完需要 18 秒。一开始的优化思路是全部懒加载——首屏只加载当前可见的子应用。结果用户切换 Tab 时每个子应用都要等 2-3 秒体验从慢变成了卡。微前端子应用的加载策略本质上是带宽预算分配问题在总带宽有限的前提下什么时候加载哪个子应用才能让首屏可交互时间和切换响应时间两个指标都达标。二、三层加载策略按用户行为概率分配带宽graph TD A[用户访问页面] -- B{当前可见br/子应用?} B --|是| C[Layer 1: 立即加载br/Eager Load] B --|否| D{用户大概率br/会访问?} D --|是(概率 60%)| E[Layer 2: 预取br/Prefetch] D --|否| F[Layer 3: 懒加载br/Lazy Load] C -- C1[同步加载br/阻塞首屏渲染] C -- C2[关键渲染路径br/优先级: 最高] E -- E1[idle 时加载br/优先级: 低] E -- E2[requestIdleCallback] E -- E3[或 link relprefetch] F -- F1[用户触发时加载br/优先级: 中] F -- F2[动态 import()] F -- F3[带 Loading 占位] style C fill:#FF6B6B,color:#fff style E fill:#F5A623,color:#000 style F fill:#50B86C,color:#fff三层策略的核心逻辑Layer 1立即加载首屏可见的子应用在主应用的head中使用link relpreload声明浏览器在解析 HTML 时即开始下载优先级最高。Layer 2预取用户大概率会访问的子应用如常用 Tab、菜单里前 3 项在浏览器空闲时通过requestIdleCallback或link relprefetch下载不影响首屏渲染。Layer 3懒加载低频子应用在用户实际点击时通过dynamic import()加载配合 Loading 骨架屏优化感知延迟。三、生产级加载调度器实现/** * 微前端子应用加载调度器 * 根据可见性、用户行为预测和当前带宽决定子应用的加载策略 */ interface SubAppLoadConfig { name: string; entry: string; // 用户访问该子应用的历史概率基于埋点数据分析 visitProbability: number; // 0-1 // 是否需要立即加载首屏可见区 isCriticalVisible: boolean; // 子应用大小预估KB用于带宽预算计算 estimatedSizeKB: number; } type LoadStrategy eager | prefetch | lazy; class SubAppLoadScheduler { // 当前带宽预算KB基于网络信息 API 估算 private bandwidthBudgetKB: number 5000; // 已分配的大小 private allocatedKB: number 0; // 预取队列 private prefetchQueue: SubAppLoadConfig[] []; // 已注册的子应用 private registeredApps: Mapstring, SubAppLoadConfig new Map(); // 加载状态 private loadedApps: Setstring new Set(); private loadingApps: Setstring new Set(); constructor() { this.estimateBandwidth(); this.startPrefetchLoop(); } /** * 注册子应用并决定加载策略 */ register(app: SubAppLoadConfig): void { this.registeredApps.set(app.name, app); const strategy this.determineStrategy(app); switch (strategy) { case eager: this.loadEager(app); break; case prefetch: this.prefetchQueue.push(app); break; case lazy: // 不主动加载由路由触发 break; } } /** * 根据多种因素决策加载策略 */ private determineStrategy(app: SubAppLoadConfig): LoadStrategy { // 规则 1: 首屏可见 → 立即加载 if (app.isCriticalVisible) { return eager; } // 规则 2: 剩余带宽充足 访问概率高 → 预取 const remainingBudget this.bandwidthBudgetKB - this.allocatedKB; if (app.visitProbability 0.6 app.estimatedSizeKB remainingBudget * 0.5) { return prefetch; } // 规则 3: 访问概率中高 带宽宽裕 → 预取 if (app.visitProbability 0.3 app.estimatedSizeKB remainingBudget * 0.3) { return prefetch; } // 其余 → 懒加载 return lazy; } /** * 立即加载 (Eager Load) */ private loadEager(app: SubAppLoadConfig): void { if (this.loadedApps.has(app.name) || this.loadingApps.has(app.name)) { return; } this.loadingApps.add(app.name); this.allocatedKB app.estimatedSizeKB; // 使用 link relpreload 声明关键资源 // 为什么用 preload 而非直接 import() // preload 在 HTML 解析阶段就开始下载不阻塞 DOM 解析 // 且浏览器会给 preload 资源最高下载优先级 const link document.createElement(link); link.rel preload; link.as script; link.href app.entry; link.onload () { // preload 完成后通过 import() 执行脚本 // import() 会命中浏览器缓存实际不会再次下载 import(/* webpackIgnore: true */ app.entry) .then(() { this.loadedApps.add(app.name); this.loadingApps.delete(app.name); }) .catch((err) { console.error(加载子应用 ${app.name} 失败:, err); this.loadingApps.delete(app.name); }); }; document.head.appendChild(link); } /** * 预取 (Prefetch)空闲时加载 */ private startPrefetchLoop(): void { // 为什么用 requestIdleCallback 而非 setTimeout // requestIdleCallback 只在浏览器空闲时执行回调 // 不会影响用户交互和渲染帧率 const prefetch () { if (this.prefetchQueue.length 0) { requestIdleCallback(prefetch); return; } // 必须记录 deadline避免单次 idle 时间过长 const deadline (window as any).__idleDeadline; if (deadline deadline.timeRemaining() 5) { // idle 时间不足 5ms等待下一帧 requestIdleCallback(prefetch); return; } const app this.prefetchQueue.shift()!; this.prefetchApp(app).finally(() { requestIdleCallback(prefetch); }); }; requestIdleCallback(prefetch); } private async prefetchApp(app: SubAppLoadConfig): Promisevoid { if (this.loadedApps.has(app.name) || this.loadingApps.has(app.name)) { return; } this.loadingApps.add(app.name); this.allocatedKB app.estimatedSizeKB; // 使用 link relprefetch 声明低优先级预取 // 与 preload 的区别 // prefetch 是下一个页面可能需要的资源优先级低 // preload 是当前页面需要的资源优先级高 const link document.createElement(link); link.rel prefetch; link.as script; link.href app.entry; link.onload () { // prefetch 完成后在空闲时执行脚本 requestIdleCallback(() { import(/* webpackIgnore: true */ app.entry).then(() { this.loadedApps.add(app.name); this.loadingApps.delete(app.name); }); }); }; document.head.appendChild(link); } /** * 路由切换时触发的懒加载 */ async loadOnDemand(appName: string): Promisevoid { const app this.registeredApps.get(appName); if (!app) { throw new Error(子应用 ${appName} 未注册); } if (this.loadedApps.has(appName)) { return; // 已通过 prefetch 预加载 } // 已预取但未执行直接 import 命中缓存 return import(/* webpackIgnore: true */ app.entry).then(() { this.loadedApps.add(appName); this.loadingApps.delete(appName); }); } /** * 基于 Network Information API 估算可用带宽 */ private estimateBandwidth(): void { const conn (navigator as any).connection; if (!conn) { this.bandwidthBudgetKB 5000; // 默认 5MB return; } // effectiveType: slow-2g, 2g, 3g, 4g switch (conn.effectiveType) { case slow-2g: this.bandwidthBudgetKB 500; break; case 2g: this.bandwidthBudgetKB 1000; break; case 3g: this.bandwidthBudgetKB 3000; break; case 4g: this.bandwidthBudgetKB 8000; break; default: this.bandwidthBudgetKB 5000; } // 监听网络变化动态调整预算 conn.addEventListener(change, () this.estimateBandwidth()); } }四、三层策略的边界与反模式缺点预取误判的成本如果用户行为预测不准确预取的子应用资源白白消耗了带宽和缓存空间。需要基于真实的用户行为数据埋点而非直觉来确定visitProbability。prefetch 在移动端的限制iOS Safari 对link relprefetch的支持有限移动端可能回退到requestIdleCallbackimport()但 idle 回调在移动端的执行频率低于桌面端。子应用间依赖共享如果两个子应用共享react等依赖分开加载会导致重复下载。必须配合 Module Federation 的 shared scope 机制去重。禁用场景子应用数量 ≤ 2三层策略的复杂度超过收益直接全部 eager load。需要离线工作的 PWA预取策略依赖网络状态离线场景不适用。五、总结微前端子应用加载的优化核心是用带宽预算约束加载行为。三层策略把子应用拆成立即加载、预取和懒加载三类通过preload/prefetch/requestIdleCallback三个不同的浏览器原语控制加载优先级。实际决策依赖两个关键变量子应用的访问概率来自埋点和当前可用带宽来自 Network Information API。策略越精细复杂度越高需要根据子应用数量做博弈——多了值得少了不值。