Skip to content

第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(首次加载,无缓存)
  // 用户感知: 从"点击导航"到"看到内容"的时间
}

这个瀑布流揭示了三个关键瓶颈:

  1. 网络阶段是最大瓶颈:子应用的 HTML + JS + CSS 下载占总耗时的 40% 以上
  2. 串行依赖严重:必须先下载 HTML 才能解析出 JS/CSS 地址,再下载执行
  3. 沙箱创建和 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 等待第一个子应用完全挂载后才开始预加载其他子应用——这意味着用户看到首屏内容不会有任何延迟,预加载是"隐形"的。

第三,自定义策略函数。 通过传入函数,开发者可以根据业务优先级将子应用分为 criticalAppNamesminorAppNames,实现精细化的预加载控制。

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) };
  },
});

基于 VitePress 构建