背景
目前现有的监控平台针对H5在线应用,提供的可监控指标较少,特别是在性能指标这块,因此当我们有H5在线应用时,没办法做到更全面的性能数据监控,更没办法针对不同的维度去做性能优化。
其实之前在携程的时候,也做过H5性能监控,但是针对指标这块,并没有非常详细地去深入研究过,正好借这个机会,好好整理了一下,现在分享给大家。
什么是应用监控
Synthetic monitoring
定义
合成监控,也称为定向或主动监控,通过模拟用户来访问或使用应用程序,通常我们会借助一些工具来监控,包括简单的ping来判断服务器是否已启动。
在H5的应用场景下,我们有以下工具可以使用:
示例
Lighthouse
 
 
添加图片注释,不超过 140 字(可选)
 
 
添加图片注释,不超过 140 字(可选)
PageSpeed Insights
PSI跑分内部也是集成了LightHouse,所以分值差异不大
 
 
添加图片注释,不超过 140 字(可选)
Real-User Monitoring
定义
真实用户监控,也称为被动监控,收集来自真实用户交易的数据的技术,因此被监控的只是真实用户正在做的事情。
示例
Lego(蚂蚁内部监控平台)
 
 
添加图片注释,不超过 140 字(可选)
如何做真实用户监控
关键性指标
Core Web Vitals
Web Vitals是Google的一项计划,目的是为了统一衡量性能的标准,并提出一些核心性能指标,以此作为参考,从而定义页面的体验质量,而这些核心性能指标,就是Core Web Vitals
(目前最新的三个指标是在2020年定义的)
 
 
添加图片注释,不超过 140 字(可选)
这些指标的价值?
 
 
添加图片注释,不超过 140 字(可选)
如何测量这些指标?
 
 
添加图片注释,不超过 140 字(可选)
Largest Contentful Paint(LCP)
 
 
添加图片注释,不超过 140 字(可选)
一、定义
最大内容绘制完成时间,即在用户的可见窗口内的最大的图片元素或块级元素的渲染时间,主要考虑的元素有以下这些
  • <img>元素
  • <image>元素内的<svg>元素
  • <video>元素
  • CSS中通过url()加载背景图片的元素
  • 包含文本节点或其他内联级文本元素子级的块级元素
二、作用
该指标用来衡量页面加载的性能体验。
三、价值&重要性
  • 对于load或DOMContentLoaded这些指标,并不能表示用户已经看到了页面主要内容;
  • 对比First Contentful Paint(FCP),FCP也仅仅是捕获的最开始的加载体验耗时;
  • 对比First Meaningful Paint(FMP),FMP含义太抽象了,指标比较复杂,难以计算;
四、分值
  • <2.5S:较优
  • 2.5S ~ 4S:待提升
  • >4S:较差
五、示例
 
 
添加图片注释,不超过 140 字(可选)
六、获取
new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { console.log('LCP candidate:', entry.startTime, entry); } }).observe({type: 'largest-contentful-paint', buffered: true});
First Input Delay(FID)
 
 
添加图片注释,不超过 140 字(可选)
一、定义
首次输入延迟,即从用户第一次与页面交互(如点击链接、点击按钮或使用自定义的由JavaScript自驱动的控件)到浏览器实际能够开始处理事件的时间间隔。
二、作用
该指标用来衡量页面的交互的性能体验。
三、价值&重要性
  • 第一次的交互延迟将会大大地影响用户对于页面质量的整体印象,因此这段耗时很重要
  • 通常最大的交互问题往往发生在页面初始化加载的过程中
四、分值
  • <100ms:较优
  • 100ms ~ 300ms:待提升
  • >300ms:较差
五、示例
 
 
添加图片注释,不超过 140 字(可选)
通常情况下,页面加载时会发出多个请求去加载资源(CSS、JS),黄色代表此时主线程正在忙碌中
 
 
添加图片注释,不超过 140 字(可选)
这个时候其实已经有部分页面内容加载出来了(FCP),但是距离页面真正可交互时间Time to Interactive(TTI)的节点还有一段时间。
 
 
添加图片注释,不超过 140 字(可选)
而此时如果用户进行了点击或输入操作,主线程实际上会将该任务挂起,在部分资源加载执行完成后才执行该任务,因此这时候就有了First Input Delay(FID)耗时。
(为什么FID只计算点击或输入操作延时?而不是页面滚动、手势滑动这些操作)
 
 
添加图片注释,不超过 140 字(可选)
原因是因为浏览器的Event Loop事件模型决定了这两类的区别,不同类型的事件处理方式是不同的,点击和输入是属于离散事件(不连续的),而页面滚动这类是属于连续事件,它们会和UI事件任务队列进行调度、合并,浏览器会有单独的UI线程去处理这些任务,因此不会有较大的延迟,因此我们需要重点关注的是那些离散事件。
六、获取
new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { const delay = entry.processingStart - entry.startTime; console.log('FID candidate:', delay, entry); } }).observe({type: 'first-input', buffered: true});
Cumulative Layout Shift(CLS)
 
 
添加图片注释,不超过 140 字(可选)
一、定义
累积布局偏移,即量化用户在整个页面生命周期过程中发生的布局偏移的情况。
二、作用
该指标用来衡量页面视觉的稳定性。
三、价值&重要性
视频封面
 

上传视频封面

 
好的标题可以获得更多的推荐及关注者
 
四、分值
  • <0.1:较优
  • 0.1 ~ 0.25:待提升
  • >0.25:较差
五、示例
得分方式
 
 
添加图片注释,不超过 140 字(可选)
impact fraction:影响分数,即前一帧和后一帧对比,所有位置发生变化的元素,在可视窗口内所影响的面积占窗口总面积的比例;
distance fraction:距离分数,即前一帧和后一帧对比,所有位置发生变化的元素,它们在屏幕竖直方向或水平方向上发生的位移(唯一的距离去较大的),占可视窗口的总高度/宽度的比例;
举个例子
 
 
添加图片注释,不超过 140 字(可选)
这个元素在加载过程中发生了位置的偏移,该元素整体面积大小占屏幕尺寸的50%,大概偏移了25%的高度,因此整个受影响的区域面积占比即为75%,因此impact fraction(影响分数)即为0.75;
而竖直方向位移了25%,因此distance fraction(距离分数)即为0.25;
所以这次CLS最终计算得分是:0.75 * 0.25 = 0.1875
 
六、获取
let clsValue = 0; let clsEntries = []; let sessionValue = 0; let sessionEntries = []; new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { // Only count layout shifts without recent user input. if (!entry.hadRecentInput) { const firstSessionEntry = sessionEntries[0]; const lastSessionEntry = sessionEntries[sessionEntries.length - 1]; // If the entry occurred less than 1 second after the previous entry and // less than 5 seconds after the first entry in the session, include the // entry in the current session. Otherwise, start a new session. if (sessionValue && entry.startTime - lastSessionEntry.startTime < 1000 && entry.startTime - firstSessionEntry.startTime < 5000) { sessionValue += entry.value; sessionEntries.push(entry); } else { sessionValue = entry.value; sessionEntries = [entry]; } // If the current session value is larger than the current CLS value, // update CLS and the entries contributing to it. if (sessionValue > clsValue) { clsValue = sessionValue; clsEntries = sessionEntries; // Log the updated value (and its entries) to the console. console.log('CLS:', clsValue, clsEntries)}}}}).observe({type:'layout-shift', buffered:true});
坑点:兼容性问题
上面的三个指标存在较大的兼容性问题,经试验后发现,在苹果真机上表现极差,数据基本取不到,因此无奈只能放弃。
Lighthouse Performance Scoring
First Contentful Paint(FCP)
 
 
添加图片注释,不超过 140 字(可选)
一、定义
从页面开始加载,一直到页面有第一个DOM内容绘制出现的时间,“DOM内容”是指文本、图像(包含背景图片)、<svg>元素或非白色的<canvas>元素。
二、作用
该指标用来衡量用户感知的加载速度。
三、分值
  • <1.8S:较优
  • 1.8S ~ 3S:待提升
  • >3S:较差
四、示例
 
 
添加图片注释,不超过 140 字(可选)
五、获取
new PerformanceObserver((entryList) => { for (const entry of entryList.getEntriesByName('first-contentful-paint')) { console.log('FCP candidate:', entry.startTime, entry); } }).observe({type: 'paint', buffered: true});
坑点:兼容性问题,放弃了
First Meaningful Paint(FMP)
一、定义
首次有意义的绘制。
二、作用
该指标用来衡量页面主要内容的加载速度,"主要内容"的。
三、分值
由于该指标受具体业务场景因素影响,所以以下分值仅供参考。
 
 
添加图片注释,不超过 140 字(可选)
四、示例
 
 
添加图片注释,不超过 140 字(可选)
五、获取
无明确算法来获取FMP,因为本身对于“有意义的内容”定义比较抽象,所以Google并没有提供相关的API。
坑点:无法自动获取,建议手动获取上报
Time To Interactive(TTI)
一、定义
页面可交互时间,即从页面开始加载,一直到用户可以自由输入或操作页面的时间。
二、作用
该指标用来衡量一个页面真正可用的时间,而并不仅仅是可看,输入框是否可输入、表单是否可提交等。
三、分值
同FMP,该指标也偏主观性,所以分值也仅供参考。
 
 
添加图片注释,不超过 140 字(可选)
四、示例
 
 
添加图片注释,不超过 140 字(可选)
五、获取
同FMP,无明确API,只能通过Google的相关工具来检测。
坑点:无法自动获取,建议手动获取上报
Performance Timeline
Performance Timeline是W3C性能小组提出的一个规范,它提供了一系列的API接口用来测量页面的性能指标和数据。
Level 1:PerformanceTiming
该接口是为保持向后兼容性而保留的传统接口,并且提供了在加载和使用当前页面期间发生的各种事件的性能计时信息。
 
 
添加图片注释,不超过 140 字(可选)
 
指标名
含义
navigationStart
从同一个浏览器上下文的上一个文档卸载(unload)结束时的UNIX时间戳,单位是毫秒。如果没有上一个文档,这个值会和PerformanceTiming.fetchStart相同。
unloadEventStart
unload事件抛出时的UNIX时间戳,单位毫秒。如果没有上一个文档,或前一个网页与当前网页不同域名, 那么这个值会返回0。
unloadEventEnd
机制同unloadEventStart,区别是它多一个callback执行时间。
redirectStart
第一个HTTP重定向开始时的UNIX时间戳。如果没有重定向,或者重定向中的一个不同源,这个值会返回0。
redirectEnd
最后一个HTTP重定向完成时(也就是说是HTTP响应的最后一个比特直接被收到的时间)的UNIX时间戳。如果没有重定向,或者重定向中的一个不同源,这个值会返回0。
fetchStart
浏览器准备好使用HTTP请求来获取(fetch)文档的UNIX时间戳。这个时间点会在检查任何应用缓存之前。
domainLookupStart
域名查询开始的UNIX时间戳。如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和fetchStart一致。
domainLookupEnd
域名查询结束的UNIX时间戳,机制同上。
connectStart
返回HTTP请求开始向服务器发送时的Unix毫秒时间戳。如果使用持久连接(persistent connection),则返回值等同于fetchStart属性的值
connectEnd
返回的是连接建立之后的时间戳,连接建立指的是所有握手和认证过程全部结束。
secureConnectionStart
返回浏览器与服务器开始安全链接的握手时的Unix毫秒时间戳。如果当前网页不要求安全连接,则返回0。
requestStart
返回浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的Unix毫秒时间戳。
responseStart
返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的Unix毫秒时间戳。如果传输层在开始请求之后失败并且连接被重开,该属性将会被数制成新的请求的相对应的发起时间。
responseEnd
返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时(如果在此之前HTTP连接已经关闭,则返回关闭时)的Unix毫秒时间戳。
domLoading
返回当前网页DOM结构开始解析时(即Document.readyState属性变为“loading”、相应的readystatechange事件触发时)的Unix毫秒时间戳。
domInteractive
返回当前网页DOM结构结束解析、开始加载内嵌资源时(即Document.readyState属性变为“interactive”、相应的readystatechange事件触发时)的Unix毫秒时间戳。
domContentLoadedEventStart
返回当解析器发送DomContentLoaded事件,即所有需要被执行的脚本已经被解析时的Unix毫秒时间戳。
domContentLoadedEventEnd
返回当所有需要立即执行的脚本已经被执行(不论执行顺序)时的Unix毫秒时间戳。
domComplete
返回当前文档解析完成,即Document.readyState变为'complete',且readystatechange 被触发时的Unix毫秒时间戳。
loadEventStart
返回该文档下,load事件被发送时的Unix毫秒时间戳。如果这个事件还未被发送,它的值将会是0。
loadEventEnd
返回当load事件结束,即加载事件完成时的Unix毫秒时间戳。如果这个事件还未被发送,或者尚未完成,它的值将会是0.
获取方式
const performanceTiming: PerformanceTiming = window.performance.timing; performanceTiming.navigationStart;
扩展指标
Time To First Byte(TTFB)
首字节时间,表示用户从开始访问页面url之后,到服务端返回了第一个字节的这段时间。
// TTFB time const ttfb = performanceTiming.responseStart - performanceTiming.navigationStart;
Request
http请求耗时,不包含tcp连接。
// request time const request = performanceNavigationTiming.responseEnd - performanceNavigationTiming.requestStart;
DOMContentLoaded(DCL)
初始的 HTML 文档被完全加载和解析完成的耗时。
// domcontentloaded time const dcl = performanceTiming.domContentLoadedEventStart - performanceTiming.navigationStart;
注:PerformanceTiming兼容性较好,目前真机上实践后未发现任何兼容性问题,但是官方不再建议使用该API,建议尽快升级到level 2.
Level 2
最新版的规范是W3C在今年(2021年)6月30日起草的:Performance Timeline Level 2
 
 
添加图片注释,不超过 140 字(可选)
其中它又包含三个小规范:
 
 
添加图片注释,不超过 140 字(可选)
Navigation Timing
该规范定义了 Web 应用程序的接口,用于访问文档导航的完整时间信息。
 
 
添加图片注释,不超过 140 字(可选)
注:DOMHighResTimeStamp类型:用于存储以毫秒为单位的时间值,相对于时间原点,或表示两个DOMHighResTimeStamp类型之间的时间间隔
获取方式
const performanceNavigationTimingEntries: PerformanceEntry[] = window.performance.getEntriesByType( 'navigation', ); if (performanceNavigationTimingEntries.length) { const [ performanceNavigationTimingString, ] = performanceNavigationTimingEntries; const performanceNavigationTiming: PerformanceNavigationTiming = performanceNavigationTimingString.toJSON(); const startTime = performanceNavigationTiming.startTime; }
扩展指标
由于新版的API有所变化,部分指标的计算方式也发生了改变。
// TTFB time const ttfb = performanceNavigationTiming.responseStart; // Request time const request = performanceNavigationTiming.responseEnd - performanceNavigationTiming.requestStart; // domcontentloaded time const dcl = performanceNavigationTiming.domContentLoadedEventStart;
坑点:经实践后发现,PerformanceNavigationTiming部分IOS机型上获取不到数据,存在一定兼容性问题,所以需要依赖PerformanceTiming兜底,判断依据是:
const performanceNavigationTimingEntries: PerformanceEntry[] = window.performance.getEntriesByType( 'navigation', ); if (performanceNavigationTimingEntries.length) { // 支持level 2 } else { // 不支持,兜底取level 1 }
Resource Timing
该规范定义了 Web 应用程序访问文档中资源的完整计时信息的接口。
 
 
添加图片注释,不超过 140 字(可选)
获取方式
const resourceList: PerformanceEntry[] = window.performance.getEntriesByType( 'resource', ); for (i = 0; i < resourceList.length; i++) { if (resourceList[i].initiatorType == "img") { alert("End to end resource fetch: " + (resourceList[i].responseEnd - resourceList[i].startTime)); } }
扩展指标
// 加载资源数量 const performanceResourceTimingEntries: PerformanceEntry[] = window.performance.getEntriesByType( 'resource', ); const resourceCount = performanceResourceTimingEntries.length; // 加载资源的大小 let transferSize = 0; performanceResourceTimingEntries.forEach(item => { transferSize += item.transferSize; });
坑点:transferSize在IOS上表现较差,部分机型获取不到,因此放弃了
User Timing
该规范定义了一个API,通过让他们访问高精度时间戳来帮助 Web 开发人员衡量他们的应用程序的性能。
使用方法
async function run() { performance.mark("startTask1"); await doTask1(); // Some developer code performance.mark("endTask1"); performance.mark("startTask2"); await doTask2(); // Some developer code performance.mark("endTask2"); // Log them out const entries = performance.getEntriesByType("mark"); for (const entry of entries) { console.table(entry.toJSON()); } } run();
监控工具落地
关键性指标
经过不断地项目实践,最终我们汇总了一批可监控的关键性指标,并集成在了监控工具中,具体指标如下:
 
 
添加图片注释,不超过 140 字(可选)
关键性维度
同时,我们针对不同的指标,也统计了多个维度去分析不同维度对指标的数值的影响,具体维度如下:
 
 
添加图片注释,不超过 140 字(可选)
更多监控细节
一. 时间的获取
在我们的监控工具中,获取时间均采用的是performance.now() API,为什么不用Date.now()
  • 精度更高:performance.now() 返回的时间戳精度最高可达微秒级,Date.now() 毫秒级
  • 延时更低:performance.now() 是以一个恒定的速率慢慢增加的,它不会受到系统时间的影响
二. 采样率
为了避免大流量的H5页面上报数据较多,我们提供了sampleRate采用率的配置,默认是10%
三. 上报的时机
针对上报的时机,我们也讨论了很多,最终确定在三个场景下,会选择自动上报
  1. 一般情况下,优先考虑当页面执行到onload事件时,3秒后会自动收集所有数据,然后判断此时是否有TTI时间
  2. 有,则上报;
  3. 没有,则继续等待;
  4. 如果用户在加载页面的时候突然离开或刷新页面,此时会进一步判断:
  5. 此时页面如果还未执行到onload,则视为无效数据监控,直接放弃;
  6. 如果已经执行到onload,进一步判断是否过了3秒:
  7. 过了,直接上报,此时如果没有TTI,则兜底此刻为TTI;
  8. 没过,则继续判断是否有TTI
  9. 有,直接上报
  10. 没有,直接放弃
  11. 极端情况下,用户加载较慢,也没有提前离开或刷新页面,此时会做一个超时上报处理,超过10秒不再上报,视为垃圾数据;
 
附上部分核心代码片段
export function getTimeNow() { if (typeof window === 'undefined' || !window.performance) { console.error('it must be in the browser environment'); return; } return window.performance.now(); } /** * h5性能埋点: L */ export function logH5PerformanceL() { (window as IWindow).L = getTimeNow(); } /** * h5性能埋点: TTI */ export function logH5PerformanceTTI() { (window as IWindow).TTI = getTimeNow(); } /** * h5性能埋点: FMP */ export function logH5PerformanceFMP() { (window as IWindow).FMP = getTimeNow(); } // 上报 const log = async () => { if ((window as IWindow).LOG) return; (window as IWindow).LOG = true; // 采样率 if (options?.sampleRate && Math.random()>= options.sampleRate)return;// 若未获取到TTI,则兜底TTIif(!(window as IWindow).TTI)logH5PerformanceTTI();try{// 获取关键性指标const logItem =awaitgetH5LogItem();// 调用上报API perf.log(logItem);}catch(e){ console.error(`report h5performance log failed -${e.message}`, e);}};// onload监听事件constonload=()=>{logH5PerformanceL();setTimeout(()=>{// 先检查是否有TTI时间if(!(window as IWindow).TTI)return;log();},3000);};// onbeforeunload监听事件constonbeforeunload=async()=>{if(!(window as IWindow).L)return;if(getTimeNow()-(window as IWindow).L<3000){// 此时已获取到TTI,则直接上报if((window as IWindow).TTI){log();}return;}log();};// 10000ms超时处理setTimeout(()=>{// 极端情况,不再上报(window as IWindow).LOG=true;},10000); window.addEventListener('load', onload,{ once:true}); window.addEventListener('beforeunload', onbeforeunload,{ once:true});
最后
以上就是这次分享的全部内容了,部分指标的获取方式没有给出相应的代码,原因是调用了公司内部的JSAPI,这里就不贴出来了,但是具体的指标和维度,文档中已经给出了,大家可以自行找途径获取。
性能优化是一个老生常谈的话题了,道路崎岖且漫长,走好这条路的第一步,就是有一个完善的监控体系和多维度的数据报表,所以一定不要忽视这块内容。
posted on 2025-05-29 14:54  言先生  阅读(60)  评论(0)    收藏  举报