Appearance
第17章 微前端性能工程
"性能优化的最高境界,不是让代码跑得更快——是让用户感知不到等待的存在。"
本章要点
- 理解微前端场景下首屏性能的特殊性:预加载与懒加载的工程权衡
- 掌握公共依赖提取与共享的三大策略:externals、Module Federation shared、Import Maps
- 深入分析 Proxy 沙箱的性能开销,建立基准测试方法论
- 系统性优化 Core Web Vitals(LCP / FID / CLS)在微前端架构下的表现
微前端架构给团队带来了独立部署和技术栈自由的巨大收益——但天下没有免费的午餐。
想象这样一个场景:你的主应用加载完毕,用户点击导航切换到订单子应用。浏览器开始下载子应用的 HTML、解析其中的 JS 和 CSS 资源、创建 Proxy 沙箱、执行子应用的 bootstrap 生命周期、最后 mount 渲染到 DOM 上。这一系列动作的总耗时,决定了用户在点击之后看到内容之前,盯着白屏或 loading 动画的时长。
在单体 SPA 中,所有代码在首次加载时就已经被打包进 bundle——路由切换只是组件的替换,几乎是瞬时的。但微前端把这个"已打包"的前提打破了。每个子应用是一个独立的部署单元,它的资源需要在激活时从网络加载。这就像把一栋完整的大楼拆成了可拼接的模块化板房——灵活了,但拼接的时候需要时间。
本章的目标是:让这个"拼接时间"尽可能接近零。
下图展示了微前端子应用加载的完整瀑布流,标注了各阶段的典型耗时和优化切入点:
我们将从四个维度系统性地剖析微前端的性能工程:首屏加载策略、公共依赖共享、沙箱运行时开销、以及 Core Web Vitals 的针对性优化。每个维度都会深入源码实现,给出可落地的优化方案和真实的性能数据。
17.1 首屏性能:预加载 vs 懒加载的权衡
17.1.1 微前端的加载瀑布流
在分析优化策略之前,我们先精确地理解微前端子应用加载的完整链路:
typescript
// 微前端子应用加载的完整瀑布流(以乾坤为例)
interface LoadWaterfall {
// 阶段 1: 主应用加载
mainAppLoad: {
html: number; // 主应用 HTML 下载 ~50ms
mainJs: number; // 主应用 JS 下载+执行 ~200ms
mainCss: number; // 主应用 CSS 下载 ~80ms
frameworkInit: number; // 乾坤框架初始化 ~20ms
};
// 阶段 2: 子应用资源获取(用户点击导航后触发)
subAppFetch: {
htmlFetch: number; // 获取子应用 HTML ~100ms
htmlParse: number; // 解析 HTML 提取资源列表 ~10ms
jsFetch: number; // 下载子应用 JS ~150ms
cssFetch: number; // 下载子应用 CSS ~60ms
};
// 阶段 3: 沙箱与执行
sandboxAndExec: {
sandboxCreate: number; // 创建 Proxy 沙箱 ~5ms
jsExec: number; // 执行子应用 JS ~100ms
bootstrap: number; // 子应用 bootstrap 生命周期 ~30ms
mount: number; // 子应用 mount 渲染 ~80ms
};
// 总耗时: 约 800-1200ms(首次加载,无缓存)
// 用户感知: 从"点击导航"到"看到内容"的时间
}这个瀑布流揭示了三个关键瓶颈:
- 网络阶段是最大瓶颈:子应用的 HTML + JS + CSS 下载占总耗时的 40% 以上
- 串行依赖严重:必须先下载 HTML 才能解析出 JS/CSS 地址,再下载执行
- 沙箱创建和 JS 执行不可并行:沙箱必须在 JS 执行之前准备好
优化策略的核心思路就是:打破串行瓶颈,将资源获取前置。
17.1.2 乾坤的 prefetchApps 实现
乾坤提供了 prefetchApps API 来预加载子应用资源。它的实现值得仔细研究——不仅因为它解决了首屏性能问题,更因为它展示了一种精妙的调度策略。
typescript
// 源码位置: qiankun/src/prefetch.ts
// 乾坤的预加载策略实现
import { importEntry } from 'import-html-entry';
/**
* 预加载策略的核心:利用浏览器空闲时间预取子应用资源
* 关键洞察:使用 requestIdleCallback 而非立即加载,
* 确保预加载不影响当前页面的首屏渲染
*/
function prefetch(
entry: string,
opts?: ImportEntryOpts
): void {
// 不是立即加载,而是等浏览器空闲
if (!navigator.onLine) {
// 离线环境下跳过预加载——这是一个容易忽略的边界条件
return;
}
requestIdleCallback(async () => {
// importEntry 会获取 HTML 并解析出 JS/CSS 资源列表
const { getExternalScripts, getExternalStyleSheets } = await importEntry(
entry,
opts
);
// 再次利用 requestIdleCallback 分批加载静态资源
requestIdleCallback(() => getExternalStyleSheets());
requestIdleCallback(() => getExternalScripts());
});
}
/**
* 根据配置决定预加载哪些子应用
*/
export function doPrefetchStrategy(
apps: AppMetadata[],
prefetchStrategy: PrefetchStrategy,
importEntryOpts?: ImportEntryOpts
): void {
// 策略类型判断
if (Array.isArray(prefetchStrategy)) {
// 精确指定需要预加载的子应用列表
const appsToPrefetch = apps.filter((app) =>
prefetchStrategy.includes(app.name)
);
prefetchAfterFirstMounting(appsToPrefetch, importEntryOpts);
} else if (typeof prefetchStrategy === 'function') {
// 自定义预加载策略函数——最大灵活度
const {
criticalAppNames = [],
minorAppNames = [],
} = prefetchStrategy(apps);
// 关键子应用立即预加载
prefetchImmediately(
apps.filter((app) => criticalAppNames.includes(app.name)),
importEntryOpts
);
// 次要子应用等第一个子应用挂载后再预加载
prefetchAfterFirstMounting(
apps.filter((app) => minorAppNames.includes(app.name)),
importEntryOpts
);
} else if (prefetchStrategy === true) {
// 默认策略:第一个子应用挂载后,预加载所有其他子应用
prefetchAfterFirstMounting(apps, importEntryOpts);
}
}
/**
* 核心:等第一个子应用挂载完成后再预加载其他子应用
* 这避免了预加载与首屏渲染争抢带宽
*/
function prefetchAfterFirstMounting(
apps: AppMetadata[],
opts?: ImportEntryOpts
): void {
// 监听第一个子应用挂载完成的事件
if (window.__POWERED_BY_QIANKUN__FIRST_APP_MOUNTED__) {
apps.forEach(({ entry }) => prefetch(entry, opts));
return;
}
// 订阅首次挂载事件
window.addEventListener(
'single-spa:first-mount',
function listener() {
// 获取所有未加载的子应用
const notLoadedApps = apps.filter(
(app) => getAppStatus(app.name) === NOT_LOADED
);
notLoadedApps.forEach(({ entry }) => prefetch(entry, opts));
window.removeEventListener('single-spa:first-mount', listener);
}
);
}下图展示了乾坤预加载策略的两级调度时序:
这段代码有三个设计精妙之处:
第一,两级 requestIdleCallback 调度。 第一级等待浏览器空闲后获取 HTML 并解析资源列表,第二级再在空闲时分别加载 JS 和 CSS。这确保了预加载永远不会阻塞用户的正常交互。
第二,首屏优先原则。 prefetchAfterFirstMounting 等待第一个子应用完全挂载后才开始预加载其他子应用——这意味着用户看到首屏内容不会有任何延迟,预加载是"隐形"的。
第三,自定义策略函数。 通过传入函数,开发者可以根据业务优先级将子应用分为 criticalAppNames 和 minorAppNames,实现精细化的预加载控制。
17.1.3 实战:定制预加载策略
理论很美好,但落地时需要根据具体业务场景选择策略。以下是三种典型场景的最佳实践:
typescript
import { registerMicroApps, start } from 'qiankun';
// 场景 1: 电商平台——基于用户行为的预测性预加载
start({
prefetch: (apps) => {
// 从首页出发,用户最可能访问商品详情页
// 数据来源: 埋点分析,70% 的用户下一步点击商品
return {
criticalAppNames: ['product-detail'],
minorAppNames: ['shopping-cart', 'user-center', 'order-list'],
};
},
});
// 场景 2: 企业后台——基于角色的预加载
start({
prefetch: (apps) => {
const userRole = getCurrentUserRole();
if (userRole === 'admin') {
// 管理员最常访问系统设置和用户管理
return {
criticalAppNames: ['system-settings', 'user-management'],
minorAppNames: apps
.map((a) => a.name)
.filter(
(n) => !['system-settings', 'user-management'].includes(n)
),
};
}
if (userRole === 'operator') {
// 运营人员最常访问数据看板和内容管理
return {
criticalAppNames: ['dashboard', 'content-management'],
minorAppNames: ['order-management', 'customer-service'],
};
}
// 默认策略
return { criticalAppNames: [], minorAppNames: apps.map((a) => a.name) };
},
});
// 场景 3: 移动端 H5——网络感知的保守策略
start({
prefetch: (apps) => {
const connection = (navigator as any).connection;
if (connection) {
// 4G/WiFi 环境: 积极预加载
if (connection.effectiveType === '4g') {
return {
criticalAppNames: apps.map((a) => a.name),
minorAppNames: [],
};
}
// 3G 环境: 只预加载最关键的一个子应用
if (connection.effectiveType === '3g') {
return {
criticalAppNames: [apps[0]?.name].filter(Boolean),
minorAppNames: [],
};
}
// 2G/slow-2g 环境: 完全不预加载,节省带宽
return { criticalAppNames: [], minorAppNames: [] };
}
// 无法检测网络: 采用保守策略
return { criticalAppNames: [], minorAppNames: apps.map((a) => a.name) };
},
});