qiankun微前端实战:从接入到优化的完整踩坑指南

Newb.卜楷 前端 阅读 2,402
赞 24 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

上个月接手一个老项目,主应用用 qiankun 搞了 5 个子应用,开发环境跑起来还行,一到测试环境就卡成 PPT。用户点一下菜单,等 3 秒才加载出子应用,有时候直接白屏。我本地测还好,但测试环境网络慢一点,首屏加载时间飙到 5 秒以上,运维同事都来问是不是前端又搞什么大动作了。

qiankun微前端实战:从接入到优化的完整踩坑指南

其实不是代码逻辑问题,就是子应用加载太慢。每个子应用都是独立打包的,主应用一启动就一股脑去加载所有子应用的 entry HTML,哪怕用户根本没点进去。更离谱的是,有些子应用还带了十几兆的图表库,光 JS 就 4MB,浏览器直接卡住。

找到瓶颈了!

我先用 Chrome DevTools 的 Performance 面板录了一次加载过程,发现主线程被大量解析 JS 和 CSS 占满,而且 Network 面板里一堆子应用的 HTML、JS、CSS 并行请求,互相抢带宽。特别是那个带 ECharts 的子应用,一个 vendor.js 就 3.8MB,加载花了 2.1 秒。

再看 qiankun 的源码,发现默认的 loadApp 是在注册子应用时就触发的,不管用户是否访问。这明显是“预加载过度”——我们不需要一上来就把所有子应用都塞进内存。

另外,子应用之间还有重复依赖,比如主应用和两个子应用都用了 lodash,结果 bundle 里各打一份,白白增加体积。

核心优化:懒加载 + 公共依赖提取

折腾了半天,最后靠两招搞定:

  • 子应用按需加载:用户点菜单时才加载对应子应用
  • 提取公共依赖:主应用统一提供 React、lodash 等,子应用 externals 掉

先说懒加载。qiankun 官方其实支持动态注册,但文档写得比较隐晦。我改了主应用的注册逻辑,把子应用的注册延迟到路由切换时:

// 主应用 - 原来的写法(一上来全注册)
const apps = [
  { name: 'app1', entry: '//localhost:8081', container: '#subapp-viewport', activeRule: '/app1' },
  { name: 'app2', entry: '//localhost:8082', container: '#subapp-viewport', activeRule: '/app2' },
];
apps.forEach(registerMicroApps);
start();

// 优化后:只在需要时注册
let registeredApps = new Set();

function loadAppWhenNeeded(appName) {
  if (registeredApps.has(appName)) return;
  
  const appConfig = {
    app1: { name: 'app1', entry: '//localhost:8081', container: '#subapp-viewport', activeRule: '/app1' },
    app2: { name: 'app2', entry: '//localhost:8082', container: '#subapp-viewport', activeRule: '/app2' },
  }[appName];
  
  if (appConfig) {
    registerMicroApps([appConfig]);
    registeredApps.add(appName);
  }
}

// 在路由守卫或菜单点击处调用
window.addEventListener('hashchange', () => {
  const path = window.location.hash.slice(1);
  if (path.startsWith('/app1')) loadAppWhenNeeded('app1');
  if (path.startsWith('/app2')) loadAppWhenNeeded('app2');
});

这里注意我踩过好几次坑:不要重复注册同一个子应用,否则 qiankun 会报错。所以加了个 registeredApps Set 来记录已注册的。

然后是公共依赖。我在主应用的 webpack 配置里把 React、ReactDOM、lodash 打包成全局变量:

// 主应用 webpack.config.js
module.exports = {
  // ...
  output: {
    library: 'MainApp',
    libraryTarget: 'umd',
  },
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
    lodash: '_',
  },
};

子应用也做对应配置,不打包这些依赖:

// 子应用 webpack.config.js
module.exports = {
  // ...
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
    lodash: '_',
  },
};

同时在主应用的 HTML 里提前引入这些库:

<!-- 主应用 index.html -->
<script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>

这样每个子应用的 bundle 少了 1~2MB,效果立竿见影。

其他小优化(顺手做了)

除了上面两个大头,我还顺手干了几件事:

  • 给子应用 entry 加了缓存头(Cache-Control: max-age=31536000),避免重复下载
  • 主应用用 <link rel="prefetch"> 提前拉取可能用到的子应用 HTML(但不执行 JS)
  • 关掉子应用的 sourcemap(生产环境没必要)

prefetch 的代码长这样:

// 在主应用首页,预加载高频子应用
function prefetchSubApp(entryUrl) {
  const link = document.createElement('link');
  link.rel = 'prefetch';
  link.href = entryUrl;
  document.head.appendChild(link);
}

// 比如用户常去 app1,就提前拉
prefetchSubApp('//localhost:8081');

不过这个要谨慎用,别把冷门子应用也 prefetch 了,反而浪费带宽。

性能数据对比

优化前后测了 10 次取平均值(测试环境,4G 网络模拟):

  • 首屏加载时间(主应用 + 默认子应用):从 5.2s 降到 800ms
  • 切换到新子应用的耗时:从 3.1s 降到 400ms(首次)/ 150ms(后续)
  • 总 JS 下载量:从 12.4MB 降到 4.7MB

最明显的是,以前点菜单要盯着 loading 转半天,现在几乎秒开。虽然第一次进某个子应用还是会稍慢(毕竟要下载),但比之前好太多。

当然,也不是完美。比如用户如果快速切换多个子应用,prefetch 可能还没完成,还是会卡一下。但这种情况不多,暂时没动它。

结尾唠叨

以上是我对 qiankun 性能优化的实战总结。核心就两点:别一上来就加载所有子应用,别让每个子应用都打包公共库。这两招下去,性能提升肉眼可见。

这个方案不是最优解(比如可以进一步做子应用分块加载),但胜在简单、改动小、见效快。如果你也在用 qiankun,不妨试试。

以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流!

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论