我所在的团队中跨业务、跨平台的内容比较多,微前端越来越广泛的应用到了团队中,当前从易用性来讲还是qiankun
比较适合我的团队,但是在最近看数据的时候感觉报表上的页面性能数据好的超出了我的预期,深入思考一下,猛然想到,这些该不会都只是主应用的数据,完全没统计到子应用吧。
为什么统计不到
qiankun
微前端工程是主应用加载后再进行子应用文档的请求进行挂载的。
而主应用加载后子应用挂在前当前页面已经完成了LCP
的指标收集,也就是说还没等主体内容被渲染,页面的性能统计行为已经结束了…
为了让数据更准确,我决定追加自定义埋点,获取更可靠的性能数据。
解决思路以及问题
在网上搜索资料的时候,竟然没有发现这方面的讨论和方案,那只能完全自己动手了。
我的目标是获取子应用完全加载后的时间是一个介于LCP
和TTI
之间的指标。
关于页面指标的说明可以查看:https://web.dev/metrics/
想要达到这个目标,第一反应就是在页面挂载后如react
的componentDidMount
或vue
中的mounted
进行自定义埋点,即可统计到相对准确的性能数据。
但是想要这么做需要项目使用了router
统一管理路由,才能比较简单便捷的进行这个埋点,否则可能就要每个页面都做单独的埋点,或者对页面组件进行统一的封装,不是很通用。
而且还有一些更麻烦的场景,如:
- 一些项目是使用目录方式管理路由的,没有统一的挂载入口
- 同一个主应用不同子应用间切换,只走了挂载钩子,如何正常统计到对应的性能
下面我介绍一些我的团队实际场景中的一些解决方案。
实际方案
统一Router场景
我的团队统一router
的都是react场景,其他场景请举一反三,或评论区补充。
如果你没有使用React.lazy
进行页面组件的加载,那么只要在Router
的componentDidMount
时机进行自定义质量埋点即可.
那如果你使用了React.lazy
那router
里面componentDidMount
执行时,你的页面组件还没加载,那埋点的数据还是有误的,但是我也不想埋到每个页面上,那我还能怎么做呢?
借助Suspense
的fallback
,示例代码如下:
import React, { Suspense, useEffect } from 'react'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; const startTime = window.performance.timing.navigationStart; const RenderFallback = () => { useEffect(() => { return () => { const mountedRenderTime = +new Date() - startTime; // 在这里添加性能埋点代码 }; }, []); return <div>页面加载中,请稍后...</div>; }; const Index = (): React.ReactElement => { return ( <BrowserRouter> <Suspense fallback={<RenderFallback />}> <Routes> <Route path="/test" element={React.lazy(() => import('@/pages/test'))}> </Route> </Routes> </Suspense> </BrowserRouter> ); }; export default Index;
什么是
Suspense
?主要是用来配合
React.lazy
进行使用的,在lazy
的组件未加载好之前,通过fallback
中的内容进行临时的填充如loading
,详细内容参考:https://zh-hans.reactjs.org/docs/react-api.html#reactsuspense
通过目录自动产生路由的场景
这里以vue
的nuxt
的场景举例,其他场景请举一反三或大家评论补充。
nuxt
这个场景前面有提到,因为没有显式的路由组件进行声明,通过上面的思维就无法解决这个问题了,因此探索了一下其他的方案,我最终选择了为nuxt
增加plugin
的方式进行处理。
大概思路是为全局注入一个mixin
,如果当前是page
组件就在mounted
的时候进行埋点操作。
实现步骤
- 我在
nuxt.config.js
文件中,增加了一个performance.ts
的配置,并且关闭ssr
plugin文档请参考:https://nuxtjs.org/docs/configuration-glossary/configuration-plugins#the-plugins-property
plugins: [ { src: '@/plugins/performance-tracker.ts', ssr: false } ]
- 编写
performance.ts
import Vue from 'vue' declare global { interface Window { __POWERED_BY_QIANKUN__: any } } const startTime = window.performance.timing.navigationStart const startFromMainApp = !!window.__POWERED_BY_QIANKUN__ let SUBMIX_PERFORMANCE_TRACKER_DONE = false Vue.mixin({ mounted() { if (!(this as any).$parent && !SUBMIX_PERFORMANCE_TRACKER_DONE) { SUBMIX_PERFORMANCE_TRACKER_DONE = true const mountedRenderTime = +new Date() - startTime // 在这里添加性能埋点代码 } } })
在代码中有个我通过判断某个组件的this.$parent
为空,来确定这是个是当前页面的page
组件,而在埋点后,还要记录一下埋点状态,避免场景考虑的不周到带来的误埋点。
单页面应用性能统计
子应用间切换跟单页面跳转和上述情况还不太一样,因为会重新请求资源,子应用也要重新渲染,不关注性能也会有很大的隐患,但是乾坤本身给到的挂载钩子并不能满足统计的需要,那我如何统计呢?
上面两个方案解决了第一次进入应用时的解决方案,但是单页面应用和子应用间切换时又统计不到了。
可以在页面卸载的时候,在子应用全局注入一个新的页面起始时间,以此来进行子应用切换场景的性能埋点。
我还是以nuxt
的plugin
代码为例。
import Vue from 'vue' declare global { interface Window { __POWERED_BY_QIANKUN__: any } } let startTime = window.performance.timing.navigationStart let startFromMainApp = !!window.__POWERED_BY_QIANKUN__ let SUBMIX_PERFORMANCE_TRACKER_PATH = window.location.pathname let SUBMIX_PERFORMANCE_TRACKER_DONE = false const startRenderTime = +new Date() - startTime Vue.mixin({ mounted() { if (!(this as any).$parent && !SUBMIX_PERFORMANCE_TRACKER_DONE) { SUBMIX_PERFORMANCE_TRACKER_DONE = true const mountedRenderTime = +new Date() - startTime } }, destroyed() { // 在当前页面组件销毁时进行性能埋点的前置准备 if (SUBMIX_PERFORMANCE_TRACKER_PATH !== window.location.pathname) { SUBMIX_PERFORMANCE_TRACKER_PATH = window.location.pathname SUBMIX_PERFORMANCE_TRACKER_DONE = false // 这里可以在全局缓存一个状态表示下一个挂载的子应用是被切换过去的,这个也可以通过主应用标记 startFromMainApp = false // 这里可以将startTime注入到全局给其他子应用进行获取,进行子应用切换的性能统计 startTime = +new Date() } } })
单页面应用因为往往做了应用级别的预加载,后续页面的加载性能不是一个很复杂的课题,课题往往在页面中的交互,所以性能这部分的性能根据场景也可以选择性忽略。
单页面应用其实和微前端关系不大,只是给子应用切换做一个铺垫,下面我们来看下子应用切换的性能统计。
子应用切换的性能统计
子应用切换的成本比单页面切换的成本高不少,因为新挂载的子应用的行为,会重新请求资源,且重新渲染,不关注性能也会有很大的隐患,但是乾坤本身给到的挂载钩子并不能满足统计的需要,那我如何统计呢?
那经过前面单页面应用性能统计的启发,统计是单页面应用统计的一个升级版,差异在于需要在主应用上更新启动时间。
这个时候如果你的项目有类似的需要就封装一个更新时间的API通过给到子应用,或者干脆将主应用的window
传递给子应用(不推荐)。
window.updateStartTime = function(newTime) { window.SUBAPP_START_TIME = newTime; }; // 给子应用配置要传递的方法 { props: { updateStartTime: window.updateStartTime, // otherProps } }
结语
至此,虽然我列举的场景可能不全,但是通过上面的方案对有需要的同学应该能提供一些灵感,如果你有更好的方案可以在评论区给出你的想法。
自从上线了这套自定义性能统计的埋点,才发现之前的性能数据真的都是虚假繁荣,而没有很快发现,也是因为qiankun
主应用的性能本身也是个问题,先说到这里,我要和同学们琢磨优化的事情去了。