likes
comments
collection
share

微前端性能优化的那些事

作者站长头像
站长
· 阅读数 56

背景

实际工作中,我负责的业务系统已经迭代了很长的时间了,整个系统成一个复杂的巨石应用,整体技术体系落后,代码体积持续增加。为为了解构巨石应用,同时分模块逐步重构整个应用系统,前端我们引入了 qiankun 架构。

最终我们将核心的模块全部进行重构,并且使用 qiankun 封装成微应用,在各个业务系统和门户之间进行接入,极大降低了开发成本和维护成本,受到了开发者和伙伴部门的欢迎。

但是在实际推广过程中也是遇到一些架构层次的局限性,主要有以下三个主要局限性:

  • 1、需要设计新的权限系统,来适配各个业务系统之间的用户鉴权和授权管理。
  • 2、外部系统接入子应用时,大部分有一些定制开发的需要,如何让伙伴开发进行快速定制。
  • 3、微应用 TTI(Time to Interactive)可交互时间超过3s,用户等待感明显。

本团队针对这个三个方面都做了一些良好的设计尝试,也都想分享给大家。本文就先介绍微前端的性能优化部分,这部分相对独立,另外两部分请允许我后续发文章分享给大家。

如果大家想了解如何将微前端进行落地,可以看这篇《手把手教你用 qiankun 落地微前端利器》文章,了解一些前置知识。

优化思路

我们的核心目标是将加载时间进行压缩,让用户感受不到等待时间,因此设立以下技术核心指标:

1、FCP 400ms

微应用 FCP(First Contentful Paint)首次内容绘制: 400ms以内。 主应用 FCP 时间不受影响

2、TTI 2000ms

微应用 TTI(Time to Interactive)可交互时间 压缩到 2s以内(预加载子应用全局数据)。 主应用 TTI 要保证不受微应用的 http 的影响。

接下来梳理整个微前端框架的实际加载过程,这里花了一个微前端执行过程的草图。 微前端性能优化的那些事 对启动可能得优化点进行标注,梳理出一下的性能可以优化的地方:

  • 1、提前加载时机,进行预加载和按需加载。
  • 2、资源共享,实现微前端应用之间的前端资源共享。
  • 3、优化qiankun执行性能。
  • 4、优化微应用的首屏,进行传统网页优化。

接下来就是按照整体规划进行逐个击破,向着我们的目标前进。

各个击破

1、提前加载时机

<div
    :style="{ height: visible ? '0px' : 0 }"
    class="micro-app-wrapper"
    :class="{
      hidden: !visible,
    }"
  >
    <div v-show="loading && visible" class="micro-app-loading">
      <a-spin :spinning="loading" />
    </div>
    <div id="micro-app-container">微应用挂载节点</div>
  </div>

这是个vue主应用因为微应用的模板,请将它放到router-view这一层次,这样能够保证其首次进入页面,且组件渲染后能够进行预加载。

<micro-app-container @visibleChange="onMicroAppVisibleChange"></micro-app-container>
          <!-- 并列层级 -->
<router-view :class="{ 'app-content': !hideAppContent, 'micro-app-content': microAppVisible }" />

如果采用在路由的组件内加载就会导致只有进入到这路由页面,才会触发qiankun的加载和渲染,会导致用户要等待微应用加载完之后才能体验页面。

其次微应用的加载实际并不是越早越好,要在主应用完成加载和渲染之后的空闲时间,再启动微应用的加载。毕竟浏览器的请求并发存在限制,且qiankun是通过解析 HTML 模板,重构 XHR 的 HTTP 请求,来进行 JS 和 CSS 资源的请求,其 HTTP 的请求压力就更大了。 因此选择在 Layout 组件挂载之后进行预加载。

// container.vue
{
    mounted() {
        registerMicroApps([...], { 
            beforeLoad: () => { /** 执行动画 */ },
            afterMount: () => { /** 结束动画 */ },
        });
        start({
            prefetch: 'all',
        });
    }
}

qiankun预加载的属性是prefetch,默认为 true

  • 配置为 true 则会在第一个微应用 mount 完成后开始预加载其他微应用的静态资源
  • 配置为 'all' 则主应用 start 后即开始预加载所有微应用静态资源
  • 配置为 string[] 则会在第一个微应用 mounted 后开始加载数组内的微应用资源
  • 配置为 function 则可完全自定义应用的资源加载时机 (首屏应用及次屏应用)

因此设置为'all',这里和我们预期中的 true 的含义不同,这里需要我们认真看下 API 的说明。

2、资源共享

接下来我们将优化重点集中到qiankun的微应用加载过程。

  • 首先读取 entry 的 HTML 文件,解析其中的内容。
  • 抽取 HTML 文件内的 JS 文件请求,使用重构后的 fetch 请求进行 JS 资源拉取,并且对返回的 JS 代码进行沙箱隔离,上下文设置。
  • 抽取 HTML 文件内的 CSS 文件请求,使用重构后的 fetch 请求进行 CSS 资源拉取,并且对返回的 CSS 代码进行样式沙箱隔离(shadow DOM)。

上面的描述是简单的描述,实际过程要比这个更加复杂和科学。我们要分析这个过程中,哪些环节是可以进行优化的。

依赖库共享

常规业务系统中,微前端框架下的应用之间往往具有很高的相似性,常见的是应用之间的技术体系和UI体系是保持一致的,那么项目中的组件依赖库是具有可复用的特性的,假如我们可以将这些依赖进行提取公共部分,统一在主应用上进行一次加载,之后的所有微应用可以共享,是否可以提升微应用的加载速度呢?从这个角度出发,我进行了几个方案的提取尝试,这里我之所以将错误方案的研究也给大家讲述了,是因为我觉得这些错误的尝试过程也是宝贵的经验,常言道:失败是成功之母。在这个过程中我们也能触类旁通收获更多的知识。

依赖库共享-splitChunks方案-错误尝试 新版webpack不推荐使用CommonsChunkPlugin了,大家使用进行代码分割的时候注意下webpack的版本。 接下来直接将webpack-chain的 JS 配置给大家放出来。

提取公共 microbase chunk,将常用的工具库进行提取,当时如果技术栈统一前端框架react和UI组件库antd都是可以提取,配置如下:

    configChain.optimization.splitChunks({
      ...configChain.optimization.get('splitChunks'),
      ...{
        chunks: 'all',
        ...
        name: false,
        cacheGroups: {
          polyfill: {
            test: /[\\/]node_modules[\\/](core-js|@babel|regenerator-runtime)/,
            name: 'polyfill',
            priority: 70,
            minChunks: 1,
            reuseExistingChunk: true
          },
          vendor: {
            test: /[\\/]node_modules[\\/](redux|react-redux|redux-thunk|react-dom|react-router-dom|dayjs)/,
            name: 'vendor',
            ...
          },
          microBase: {
            test: /[\\/]node_modules[\\/](axios|moment|lodash|@tencent\/ftech-helpers|xss|uuid|qiankun)/,
            name: 'microBase',
            ...
          },
          antd: {
            test: /[\\/]node_modules[\\/](antd|@ant-design)/,
            name: 'antd',
            ...
          },
        },
      },
    });

同时关闭微应用中该 JS 的文件的引入。

const MicroBaseReg = /<script src="\/smart-contact-cms\/assets\/js\/microBase(.*?).js"><\/script>/g;

start({
    getTemplate(tpl: string) {
      const html = tpl.replace(MicroBaseReg, '');
      return html;
    },
    prefetch: 'all',
})

实际到这里大家就很明显发现这个方案是不可行的,因为我忽略了前端模块化已经是前端工程的标配了,其提取的 JS 文件的头部已经基于项目 package.jsonname生成了webpackJsonp_productName的 chunk 数组。

// 主应用的 JS 文件
((typeof self !== 'undefined' ? self : this)["webpackJsonp_industry-platform"] = (typeof self !== 'undefined' ? self : this)["webpackJsonp_industry-platform"] || []).push([["microBase"],{})
// 微应用的 JS 文件
(window["webpackJsonp_smart-contact-cms"] = window["webpackJsonp_smart-contact-cms"] || []).push([["microBase"],{

主应用所有模块放入到 webpackJsonp_industry-platform 数组,而微应用项目放入到了webpackJsonp_smart-contact-cms,相互之间无法引用。

### 依赖库共享-webpack externals方案 webpack externals能够将一些第三方包通过外部扩展的方式,通过 CDN 的方式引入到项目中,在 NPM 没有成为主流之前,前端 JS 的引入方式就是这种方式的雏形。

<script
  src="https://code.jquery.com/jquery-3.1.0.js"
  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  crossorigin="anonymous"
></script>

module.exports = {
  //...
  externals: {
    jquery: 'jQuery',
  },
};

假如应用之间存在较大的公共依赖库,例如antd/g6antd, react, jquery 等大型仓库,就可以将这些依赖通过外部 CDN 的方式引入,避免应用之间 JS 文件的重复请求。

但是有些细节需要注意,假如你将子应用中的公共 js 文件请求通过上文代码中getTemplate进行过滤,节省一次 Http 请求,你需要将qiankun的沙箱模式关闭,不然会引入不可控的bug

const MicroBaseReg = /<script src="\/smart-contact-cms\/assets\/js\/microBase(.*?).js"><\/script>/g;
start({
    getTemplate(tpl: string) {
      const html = tpl.replace(MicroBaseReg, '');
      return html;
    },
    prefetch: 'all',
    sandbox: false,
})

这里实际不将公共的 JS 文件过滤也是可以的,性能影响经过测试,发现实际不大,因为资源是网络地址是完全一样的,因此浏览器会缓存它,最大的性能开销-http请求已经被解决了。

其次是使用 CDN 加载第三方库的数量不宜过多,过多的CDN请求,会占据浏览器并发请求数,导致父应用加载速度变慢,得不偿失了。

目前的方案也是官方推荐的方案,官方承诺在 qiankun2.0给出更加智能方式使其自动化,让我们拭目以待吧。

微前端性能优化的那些事

请求数据共享

影响微应用尽快让用户体验页面的最后一个流程是数据请求阶段,页面需要后台数据到达之后才能渲染对用户有意义的页面。 那第一个思考方向是将父应用和子应用,或者子应用之间的数据进行跨应用共享,每个数据只需要请求一次,然后所有应用共享,可以极大减少用户等待数据的时间。 目前qiankun提供了initGlobalState的方案。

// 主应用
import { initGlobalState, MicroAppStateActions } from 'qiankun';


// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);


actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();

子应用这里做一些优化,将父应用传递下来的参数封装成MainAppContext,使用react.createContext 进行全局共享。

const renderApp = (props: MainAppProps) => {
  const { container } = props;
  ReactDOM.render(
    <MainAppContext.Provider value={props}>
      <Provider store={store}>
        <HashRouter basename={prefixPath}>
          <ConfigProvider locale={zhCN} prefixCls='cmsAnt'>
            <App />
          </ConfigProvider>
        </HashRouter>
      </Provider>
    </MainAppContext.Provider>,
    container ? container.querySelector('#root') : document.querySelector('#root'),
  );
};

export const mount = async (props: any) => {
  console.log('[smart-contact-cms] mount', props);
  // 删除不能序列化的内容
  normalizeObject(props.mainAppState);
  // 作为微应用运行
  renderApp(props);
};

详细细节参考《qiankun官网initGlobalState》的介绍。

3、优化qiankun执行性能

这个 topic 确实需要对qiankun的源码有足够的了解才行,之前的做的优化是参考官方issues:《# [RFC] qiankun的极致性能优化思路,与import-html-entry有关》的一个讨论来做的,写这个文章的时候本来是想分享这个的,当时最近在帖子的尾部看到官方支持了这次优化,并且开启方式十分简单。

微前端性能优化的那些事 qiankun 2.8 之后增加了性能加载模式,通过speedy: true开启,在保留沙箱的模式基础上做了window和document 实例的性能优化,加载速度确实快了点。 这个优化目前是固定到特定版本,例如2.8.0,而最新的版本2.8.3则会执行报错,本人也没去深入研究报错的具体原因,大家先按照qiankun@2.8.0即可使用这个设计。

start({
    prefetch: 'all',
    sandbox: {
      speedy: true,
    },
});

4、微应用的首屏优化

主要集中精力在提升子应用的首屏时间、调整请求加载顺序、父子应用数据共享。例如将initGlobalState深度应用到微应用代码中,减少数据请求等等。这类的方案的文章也是到处都是,因此也就不鹦鹉学舌了,大家有兴趣的自行检索即可,我日常比较关注 前端加加奇舞团 两个前端公众号,大家有兴趣也可以去看下。

结语

通过上面的设计,我们基本满足了方案之初的性能指标,具体提升的比例并没有进行深入的计算,给大家列下目前的状态。

子应用 FCP:230+ms 子应用 TTI:列表页压缩到 2s,内容编辑页压缩到 3.5s,数据共享的页面 220+ms 基本能渲染结束

微前端是个很好的前端架构,在这个基础上我们衍生了跨应用动态主题的应用场景,所有主应用可以在运行时控制其任意子应用的动态主题,进一步扩展微应用的扩展能力,方便更多的业务系统进行定制接入,有兴趣的同学可以看看我的这篇文章《高度兼容低版本的 antd 的动态主题方案》。

文章到此就结束了,非常感谢你能读到这里,拜拜👋🏻。

转载自:https://juejin.cn/post/7155754068764262414
评论
请登录