前端性能优化笔记之首屏时间采集指标的具体方法

时间:2022-05-09 14:04:40

前端性能优化笔记之首屏时间采集指标的具体方法

1写在前面

通常,我们在开发环境中进行首屏时间测试,是通过在内网中通过Chrome DevTools观察首屏时间,这样内外网络环境存在差异,导致测量的首屏时间也会有所不同。我们在开发中使用的是调试工具,而用户是直接访问的,两者的访问形式是不同的。观察首屏时间的设备有多种,而真实的用户人群不同,移动设备的型号和所处网络环境也是各异的。

那么,如何了解用户的首屏时间呢?大量用户的首屏时间分布又是怎样的呢?性能差的用户首屏时间又是多少呢?

2手动采集办法及优缺点

所谓手动采集,一般就是通过埋点的方式进行采集上报,如:我们要收集当前页面的用户停留时间,就必须采集到打开页面的时间和关闭或隐藏页面的时间,再进行计算得到停留时间并上报。

如果是电商列表页面,瀑布流型的页面,需要根据各个机型的首屏位置,估算出一个平均的首屏位置,然后进行打点上报。

手动采集的兼容性强,可以随着情况而进行变动,其次可以去中心化,各个业务模块单独负责自己的打点代码,有问题时业务程序员去排查问题即可。但是手动采集也存在一些问题,容易与业务代码严重耦合,它的覆盖率不足,业务程序员一旦忙起来,性能优化方案的实施就会延迟排后。

3自动化采集的办法及优点

自动化采集,即引入一段通用的代码来做首屏时间自动化采集,引入过程中,除了必要的配置外不需要做其他事情。独立性强,接入过程更加自动化,可以由一个公共团队来开发,试点后进行推广到各个业务团队。但是,有些个性化需求是无法得到满足的,因为在工作中总会遇到一些特殊业务场景,会遇到难以实施自动化采集的情况。

4服务端模板业务下的采集方案

有人会说现在的前端开发不都是采用web框架进行开发吗,为啥还会涉及到服务器模板呢。那是因为在一些B端业务的公司用的还是服务端模板,如Velocity、Smarty等,比如说微前端框架SSR也是用的服务端模板。

之所以会出现这种情况,这是因为后端比较重、前端偏配合,出于效率考虑前后端并没有进行解耦。这时候如果使用现在流行的web前端框架vue/react,这无疑就会增加学习成本。

前端性能优化笔记之首屏时间采集指标的具体方法

使用浏览器提供的DOMContentLoaded接口来采集首屏时间点,具体的思路是:当页面中的HTML元素被加载和解析完成后,DOMContentLoaded事件会被触发,首屏时间=DOMContentLoaded时间=DOMContentLoadedEventEnd-fetchStart时间。

当然这种采集方法不能用于SPA单页面应用业务场景,这是因为在使用Performance API接口采集的首屏时间可能是1106ms。而实际首屏时间可能就是1976ms。在SPA单页面中,用户请求一个页面时,页面会先加载index.html,加载完成后就会触发DOMContentLoaded和load。页面会相关脚本资源并通过axios异步请求数据,使用数据渲染页面主题部分,这个时候首屏才渲染完成。SPA的流行让Performance API接口失去了原先的意义,那么,这种情况下应该如何采集首屏指标呢?

当然,我们的解决方案是采用MutationObeserver采集首屏时间。

5单页面SPA应用业务场景下的采集方法

如果一个首屏页面的内容没有被组件化,那么首屏时间就无法被统计到,除非各个业务都制定一套组件标准,首屏内容必须封装成组件。前面也知道onload的时间也并非最终时间,可能在onlaod阶段,首屏还没加载完。其次,没有考虑到首屏是张图片的情况,首屏虽然加载完成了,但是图片是异步的,图片并没有进行加载。

我们想如果能够在首屏渲染过程中,把各个资源的加载时间记录到日志中,后续再通过分析,确定某个资源加载完的时间,那么就是首屏时间。

MutationObeserver接口提供了监督对DOM树所做更改的能力,它被设计为旧的MutationEvents功能的替代品,该功能是DOM3 Events规范的一部分。

当用户进入页面时,我们可以使用MutationObeserver监控DOM元素,当DOM元素发生变化时,程序会标记变化的元素,记录时间点和分数,储存在数组中。首屏指标采集到某些条件时,首屏渲染已经结束了,我们需要考虑到首屏采集终止的条件。

递归遍历DOM元素及其子元素,根据子元素所在层级设定元素权重,比如:页面DOM元素的第一层设置为1,当其被渲染时得分为1,每增加一个元素层级权重增加0.5,当第五层级元素的权重就为3.5,渲染时给出对应分数。根据前面统计到的元素层级得分,计算元素的分数变化率,获取变化率最大点对应的分数,然后找到该分数对应的时间,即为首屏时间。

  1. function CScor(el, tiers, parentScore){ 
  2.   let score = 0; 
  3.   const tagName = el.tagName; 
  4.   // 判断当前的标签元素是否为指定的标签元素 
  5.   if(!filterTagNameInTagNames(tagName)){ 
  6.     const childrenLen = el.children ? el.children.length : 0; 
  7.     // 判读子元素的长度是否大于0 
  8.     if(childrenLen>0){ 
  9.       for(let childs = el.children, len = childrenLen-1; len >= 0; len--){ 
  10.         score += calculateScore(childs[len],tiers+1,score>0) 
  11.       } 
  12.     } 
  13.     // 判断分数是否小于等于0,且父元素的分数为0 
  14.     if(score<= 0&& !parentScore){ 
  15.       if(!(el.getBoundingClintRect&& el.getBoundingClintRect().top<WH)) return 0 
  16.     } 
  17.     score += 1 + 0.5 * tiers; 
  18.   } 
  19.   return score 
  20.  
  21. function filterTagNameInTagNames(tagName){ 
  22.   return ["SCRIPT","STYLE","META","HEAD"].some(tag=>tag===tagName) 
  23.  
  24. calFinalScore(){ 
  25.   try { 
  26.     if(this.sendMark) return
  27.     const time = Date.now() - window.performance.timing.fetchStart; 
  28.     let isCheckFMP = time > 30000 || SCORE_ITEMS && SCORE_ITEMS.length > 4 && time - (SCORE_ITEMS && SCORE_ITEMS.length && SCORE_ITEMS[SCORE_ITEMS.length-1].t || 0) > 2 * CHECK_INTERVAL || ( 
  29.       SCORE_ITEMS.length > 10 && window.performance.timing.loadEventEnd !== 0 && 
  30.       SCORE_ITEMS[SCORE_ITEMS.length-1].score === SCORE_ITEMS[SCORE_ITEMS.length - 1].score 
  31.     ); 
  32.     if(this.observer && isCheckFMP){ 
  33.       this.observer.disconnect 
  34.       // 取FMP时间,默认是30001大于30s会自动被过滤 
  35.       this.fmp = record && record.t || 30001 
  36.       try { 
  37.         this.checkImgs(document.body) 
  38.         let max = Math.max(...this.imgs.map(element=>{ 
  39.           if(/^(\/\/)/.test(element)) element = "https:" + element 
  40.           try { 
  41.             return window.performance.getEntriesByName(element)[0].responseEnd || 0 
  42.           } catch (error) { 
  43.             return 0  
  44.           } 
  45.         })) 
  46.       } catch (error) { 
  47.         return 
  48.       } 
  49.     } 
  50.   } catch (error) { 
  51.     return 
  52.   } 

如果页面里包括图片,使用上面的首屏指标采集方案,结果准确吗?答案是不准确的。上述的计算逻辑主要针对的是DOM元素而做的,图片加载过程是异步,图片容器(图片的DOM元素)和内容的加载是分开的,当容器加载出来时,内容还没出来,一定要确保内容加载出来,才算是首屏。

进行个归纳,通常计算首屏时间的方法有:

  • 首屏模块标记法
  • 统计首屏内加载最慢的图片
  • 自定义首屏

首屏模块标签标记法

在首屏模块标签标记法中,首屏时间等于firstScreen - performance.timing.navigationStart;。但是在实际业务中,能够使用首屏模块标签标记法的情况比较少,大多数页面都需要通过接口拉取数据才能完整展示,因此我们会使用JavaScript 脚本来判断首屏页面内容加载情况。

  1. <!DOCTYPE html> 
  2. <html lang="en"
  3. <head> 
  4.   <meta charset="UTF-8"
  5.   <title>首屏</title> 
  6.   <script type="text/javascript"
  7.     window.pageStartTime = Date.now(); 
  8.   </script> 
  9.   <link rel="stylesheet" href="common.css"
  10.   <link rel="stylesheet" href="page.css"
  11. </head> 
  12. <body> 
  13.   <!-- 首屏可见模块1 --> 
  14.   <div class="module-1"></div> 
  15.   <!-- 首屏可见模块2 --> 
  16.   <div class="module-2"></div> 
  17.   <script type="text/javascript"
  18.     window.firstScreen = Date.now(); 
  19.   </script> 
  20.   <!-- 首屏不可见模块3 --> 
  21.   <div class="module-3"></div> 
  22.     <!-- 首屏不可见模块4 --> 
  23.   <div class="module-4"></div> 
  24. </body> 
  25. </html> 

 

统计首屏内图片完成加载的时间

在实际进行首屏加载中,加载最慢的资源文件是图片,对此我们可以将加载最慢的图片文件的时间作为首屏时间。这是因为在浏览器发起HTTP请求,在页面中建立TCP连接,但是每个页面所能建立的连接数又是有限的,使得并不能一次性将所有的图片都能进行下载和展示。

基于此种情况,我们可以在页面DOM树构建完成后去遍历首屏内所有的图片标签,并对每个图片标签的onload事件进行监听,从而计算得到所有图片中加载时间的最大值。这样就得到首屏时间=加载最慢的图片的时间点 - performance.timing.navigationStart。

自定义模块内容计算法

由于在统计首屏内遍历图片标签列表得到最大加载时间比较复杂,对此在业务中可以通过自定义模块内容,来简化计算首屏时间。如下面的做法:

忽略图片等资源加载情况,只考虑页面主要DOM

只考虑首屏的主要模块,而不是严格意义首屏线以上的所有内容

6参考文章

《前端性能优化方法与实践》

《前端优化-如何计算白屏和首屏时间》

7写在最后

本文主要介绍了首屏指标采集相关的内容,这种性能采集方案靠谱吗?当前的互联网大厂又在使用什么采集方案呢?就目前而言,上面介绍的是当前应用的最好的首屏指标采集方案,兼容了单页面应用和服务端模板的页面。

原文链接:https://mp.weixin.qq.com/s/6bf8kgzCt3akAiZJYiAFNw