跳到主要内容

性能监控与上报

2024年04月17日
柏拉文
越努力,越幸运

一、认识


在进行微前端性能监控设计之前,我们经常用于衡量 Web 性能的指标:

FCP(First Contentful Paint) 从页面开始加载到内容的任何部分呈现在屏幕上的时间,首次内容绘制FCP指标衡量从页面开始加载到页面内容的任何部分呈现在屏幕上的时间。对于此指标, 内容是指文本、图像(包括背景图像)、<svg> 元素或非白色 canvas 元素。FCP 计算方式: 从页面开始加载持续检测页面中的元素,当页面中首次呈现出内容时停止检测,上报首次元素渲染的时间。

new PerformanceObserver((entryList)=>{
for(const entry of entryList.getEntriesByName('first-contentful-paing')){
console.log('FCP candiate:', entry.startTime, entry)
}
}).observe({ type: 'paint', buffered: true });

LCP(Largest Contentful Paing) 指标报告视口中可见的最大图像或文本块的渲染时间。(相对于页面首次开始加载的时间)。LCP计算方式: 从页面开始渲染持续检测页面中用户关注的内容: 文本、图片、svgvideo,在用户进行操作前(点击、滚动、键盘)持续收集重要元素,并根据元素的大小计算出最大元素并上报

new PerformanceObserver((entryList)=>{
for(const entry of entryList.getEntries()){
console.log("LCP candiate", entry.startTime, entry);
}
}).observe({ type: 'largest-contentful-paint', buffered: true });

通过对于 FCPLCP 的了解, 可以发现 FCPLCP 都适合用来衡量整个页面首次打开的性能,但是无法衡量微前端子应用的性能。因为微前端子应用通常为页面的一部分内容,而且这部分内容可能是通过点击事件、或路由跳转触发加载,那么如何有效的衡量页面部分内容渲染性能,或者从整个页面渲染过程中微前端应用在整个过程中占用的耗时是观察微前端应用的重要指标。

总结: 由于微前端架构的原因, 常用的性能指标 FPFCPLCP 是从整个应用的角度为检测对象,并不能很好衡量单个子应用的真实性能情况。

1.1 为微前端场景定义新指标

在微前端场景下,根据 FCPLCP 的实现特征我们同样可以为微前端渲染区域的内容定义 FCPLCP 等性能指标,用于观测从事件、路由跳转等行为开始加载微应用到微应用渲染区域 FCPLCP, 与页面中的 FCPLCP 含义不同,它的时间更多的是从 loadSubApp 时间节点开始到微应用渲染区域出现 FCPLCP 元素的时间。根据微前端应用的特征,我们定义出如下新的指标:

  1. load_app_time(load application time): 为微前端子应用加载资源的耗时时间

  2. mount_app_time(mount application time): 为微前端子应用代码执行到触发 mount 函数中的 render 逻辑耗时

  3. TTFMP(Time to First Meaningful Paint): 为微前端子应用核心元素渲染完成的时间点

  4. MFFP(Micro Front-end First Paint)

  5. MFFCP(Micro front-end First Contentful Paint): 为微前端子应用首次渲染内容(文本、图片、带背景图的内容)的时间点

  6. MFLCP(Micro front-end Largest Contentful Paint): 为微前端子应用最大内容绘制的时间点

总结: SDK 上报的性能指标 FPFCPLCP 这些和 MFFPMFFCPMFLCP 指标的关系:

  1. FPFCPLCP 是站在主应用 + 子应用整个页面维度的数据

  2. MFFPMFFCPMFLCP 是站在子应用开始渲染到最终展示的维度的指标

二、load_app_time


2.1 认识

load_app_time(load application time) 是微前端子应用加载资源的耗时时间。它统计的维度是从开始加载子应用资源到子应用加载完成后的时间点。注意这里的耗时:

  • JS Entry: 为 HTML 里的资源地址加载时间

  • HTML Entry: 为 JS 入口文件的加载时间

因此, 都不包含子应用内拆分成异步 chunk 后动态加载的 script 时间。

2.2 优化

如何基于 load_app_time 指标优化?: 若遇到 load_app_time 指标比较差的情况, 可以通过 network 筛选出子应用的资源内容, 重点关注 html 入口中的资源加载耗时, js 入口中的 js 加载耗时。

三、mount_app_time


3.1 认识

mount_app_time(mount application time) 是微前端子应用代码执行到触发 mount 函数中的 render 逻辑耗时。注意这里的耗时:

  • JS Entry: 渲染时间为子应用入口 JS 执行到 mount.render渲染耗时

  • HTML Entry: 渲染时间为 htmlscript 执行完成时间到触发 mount.render 完成的时间。在开启缓存模式后第二次渲染应用仅为 mount.render 时间

注意, 这里的 render 耗时并不包括框架里渲染的异步任务,例如组件中的接口请求等。

3.2 计算

3.3 优化

如何基于 mount_app_time 指标优化?: 若遇到 mount_app_time 指标比较差的情况, 可以通过 QianKun 提供的 beforeEvalafterEval 输出对应的 script 执行耗时, 以及子应用提供的 mountrender 函数的执行耗时。

四、TTFMP


4.1 认识

TTFMP(Time to First Meaningful Paint) 是页面核心信息加载完成及元素渲染完成的时间。需要在子应用首次渲染的内容元素上增加 elementtiming = 'important-paragraph' (若不加载首屏上无法生效,在用户点击等事件加载的内容无法作为统计数据。)

TTFMP 的时间为: load_app_time + mount_app_time + 渲染特定内容的逻辑

4.2 原理

TTFMP 计算原理: TTFMP 不同于 FCPLCP 等由 Goole 定义的性能,它主要用于统计用户关心内容的渲染时间, 因为 LCP 虽然统计的为最大元素, 但不一定是用户关心的元素, 通过上述 TTFMP 的统计方式可以发现我们可以非常容易收集到定义了 elementtiing 的元素渲染时间, 因此只需要直接收集用户定义的特定元素值即可:

const targetNode = document.querySelector('xxx');
const observer = new PerformanceObserver((list)=>{
const perfEntries = list.getEntries();
perfEntries.forEach((mutation: any)=>{
if(mutation?.element && mutation?.intersectionRect && targetNode?.contains(mutation.element) && mutation?.element.getAttribute('elementtiming') === 'important-paragraph'){
if(!TTFMPMutation){
TTFMPMutation = mutation;
metric.element = mutation.element;
metric.renderTime = Math.ceil(mutation.renderTime);
}else{
const mutationArea = mutation?.intersectionRect?.width + mutation?.intersectionRect?.height;
const TTFMPArea = TTFMPMutation?.intersectionRect?.width + TTFMPMutation?.intersectionRect?.height;
if(mutationArea > TTFMPArea){
TTFMPMutation = mutation;
metric.renderTime = Math.ceil(mutation.renderTime);
metric.element = mutation.element;
}
}
}
});
});
observer.observe({ type: 'element', buffered: false });

// 点击、页面退出等行为终止计算

4.3 优化

如何基于 TTFMP 指标优化?: 遇到 TTFMP 指标比较差的情况, 优先观察 load_app_timemount_app_time 在整个耗时的占比, 若这两个指标本身已经占了比较大的部分, 那么应该优先优化这两个指标。在 load_app_timemount_app_time 两个指标占性能比较小的情况下, 观察从开始渲染到接口或者特定逻辑执行后触发该元素的渲染耗时,一般在 mount 中的 render 逻辑中。

五、MFFP


5.1 认识

MFFP(Micro Front-end First Paint) 是从子应用开始加载的时间点到子应用首次渲染的时间点之间的时间段。这个时间段也可以被视为微前端子应用白屏时间。也就是说在用户访问微前端子应用的过程中,MFFP 时间点之前,用户看到的都是没有任何内容的子应用,用户在这个阶段感知不到任何有效的工作在进行。

5.2 计算

5.3 对比

FP VS MFFP: FP 是站在主应用+子应用整个页面维度的首次渲染时间点,而 MFFP 是站在子应用开始渲染到最终展示的维度的首次渲染时间点。

六、MFFCP


6.1 认识

MFFCP(Micro front-end First Contentful Paint) 是从子应用开始加载时间点到子应用首次渲染内容(文本、图片、带背景图内容)时间点之间的时间段。这个时间段可以被视为微前端子应用无内容时间。也就是说在用户访问微前端子应用的过程中, MFFCP 时间点之前,用户看到的都是没有任何实际内容的屏幕,用户在这个阶段获取不到任何有用的信息。

6.2 计算

FCP 的收集方式如下:

new PerformanceObserver((entryList)=>{
for(const entry of entryList.getEntriesByName('first-contentful-paint')){
console.log("FCP candidate", entry.startTime, entry);
}
}).observe({type: "paint", buffered: true});

通过上面对于 FCP 的收集方式, 我们可以了解到 FCP 主要是通过 PerformanceObserver API 进行收集。在对 MDNPerformanceObserver API 参数进行详细了解, 没有参数能够限定对于区域内容的 FCP 计算。

回到 MFFCP 指标的诉求, 希望从某个特定的时间段开始对于特定的渲染区域进行 FCP 渲染时间的统计,那么自然而然想到了 MutationObserver, 通过 MutationObserver 可以观察元素添加的回调,那么我们观察元素添加到页面中的时间并对元素节点进行分析那么就可以正确的计算出 FCP 对应的渲染时间和元素。

const observer = new MutationObserver((mutationsList)=>{
for(let i=0; i<mutationsList.length; i++){
const mutation = mutationsList[i];
if(mutation.type === 'childList'){
for(let a=0; a<mutation.addedNodes.length; a++){
const node = mutation.addedNodes[a];
// 检验是否为 FCP 关注元素
const FCPNode = node instanceof HTMLElement && includeFCPTypeContent(node);
if(FCPNode){
const endTime = performance.now();
FCPElement = FCPNode;
metric.element = node;
metric.renderTime = Math.ceil(endTime);
// 停止监控
return;
}
}
}
}
});

observer.observe(targetNoe, { subtree: true, childList: true });

最终可以将上面代码封装成以下使用方式:

import { FCPMonitor } from "@vmok/performance";

const App = ()=>{
useEffect(()=>{
const sub2 = document.querySelector(".sub2");
if(sub2){
// 传入节点,回调内可以获取 mutation
FCPMonitor(sub2, (metric)=>{
console.log("FCP", metric);
});
}
},[]);

return <div><h1></h1><h2 elementtiming='important-paragraph'></h2></div>
}

6.3 对比

FCP VS MFFCP: FCP 是站在主应用 + 子应用整个页面维度的首次内容渲染时间点,而 MFFCP 是站在子应用开始渲染到最终展示的维度的首次内容渲染时间点

七、MFLCP


7.1 认识

MFLCP(Micro front-end Largest Contentful Paint) 是从子应用开始加载时间点到子应用视口最大内容渲染时间点之间的时间段,这个时间段可以被视为微前端子应用主要内容渲染完成时间。也就是说在用户访问微前端子应用的过程中, 用于度量视口中最大的内容元素何时可见。它可以用来确定页面的主要内容何时在屏幕上完成渲染。

7.2 计算

MFLCPLCP 的核心计算逻辑主要的不同为, 在 LCP 中从页面打开到渲染期间用户开始进行操作期间内最大的渲染元素作为 LCP 元素,但是在微前端场景中需要的最大元素为从微前端应用加载到微前端区域最大的渲染内容。

new PerformanceObserver((entryList)=>{
for(const entry of entryList.getEntries()){
console.log("LCP candiate:", entry.startTime, entry)
}
}).observe({ type: "largest-contentful-paint", buffered: true });

通过对于目前浏览器的性能收集方式可以了解到没有 API 可以直接对某个区域进行渲染性能的计算,在查阅了一些资料后了解到 Element Timing API, 概述一下 Element Timing API 的能力: 在元素上设置了 elementtiming 属性后,就可以通过 PerformanceObserver 接收对应的元素渲染回调。

通过对于 Element Timming 的了解,需要对特定元素增加 elementtiming 属性, 然后通过 PerformanceObserver 对特定区域元素进行的渲染内容进行回调,计算出最大元素

// 劫持元素注入, 增加 elementtiming
const rawElementMethods = Object.create(null);
const mountElementMethods = [
'append',
'appendChild',
'insertBefore',
'insertAdjacentElement'
]
const ignoreElementTimingTags = makeMap([
'STYLE',
'SCRIPTS',
"LINK",
"META",
"TITLE"
]);

export function safeWrapper(callback: (...args: Array<any>) => any, disableWarn?: boolean){
try{
callback();
}catch(error){
console.log(error);
}
}

function injector(current: Function, methodName: string){
return function(this: Element, ...args: any[]){
const el = methodName === 'insertAdjacentElement' ? arguments[1] : arguments[0];
const originProcess = ()=> current.apply(this, args);

safeWrapper(()=>{
if(!ignoreElementTimingTags(el.tagName)){
if(el?.setAttribute && typeof el?.setAttribute === 'function' && !el?.getAttribute('elementtiming')){
el?.setAttribute('elementtiming', 'element-timing');
}
}
});

return originProcess();
}
}

export function makeElInjector(){
if((makeElInjector as any).hasInject){
return;
}

if(typeof window.Element === 'function'){
const rewrite = (methods: Array<string>, builder: typeof injector)=>{
for(const name of methods){
const fn = window.Element.prototype[name];
if(typeof fn !== 'function'){
continue;
}
rawElementMethods[name] = fn;
const wrapper = builder(fn,name);
window.Element.prototype[name] = wrapper;
}
}
rewrite(mountElementMethods, injector);
}
}

LCP 针对局部区域统计:

const targetNode = document.querySelector('xxx');
const observer = new PerformanceObserver((list)=>{
const perfEntries = list.getEntries();
perfEntries.forEach((mutation: any)=>{
if(mutation?.element && mutation?.intersectionRect && targetNode?.contains(mutation.element)){
if(!LCPMutation){
LCPMutation = mutation;
metric.element = mutation.element;
metric.renderTime = Math.ceil(mutation.renderTime);
}else{
const mutationArea = mutation?.intersectionRect?.width * mutation?.intersectionRect?.height;
const lcpArea = LCPMutation?.intersectionRect?.width * LCPMutation?.intersectionRect?.height;

if(mutationArea > lcpArea){
LCPMutation = mutation;
metric.renderTime = Math.ceil(mutation.renderTime);
metric.element = mutation.element;
}
}
}
});
});

observer.observe({ type: 'element', buffered: false });

点击、页面退出等行为终止计算

7.3 对比

LCP VS MFLCP LCP: 是站在主应用 + 子应用整个页面维度的最大内容绘制时间点。而 MFLCP 是站在子应用开始渲染到最终展示的维度的最大内容绘制时间点。它仅在主应用的接入方式中存在。

八、MFFMP(废弃)


MFFMP(Micro front-end First Meaningful Paint), 即微前端子应用首次绘制有意义内容的时间,当微前端的布局和文字内容全部渲染完成后,即可认为是完成了首次有意义内容的绘制。(精准性说明:目前尚未使用最大布局变化的时间点作为FMP, 而且简单采用了定时内子应用渲染容器内节点未发生变化,目前该值比传统 FMP 的值会更大,推荐使用 MFLCP 代替 MFFMP