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,渲染时给出对应分数。根据前面统计到的元素层级得分,计算元素的分数变化率,获取变化率最大点对应的分数,然后找到该分数对应的时间,即为首屏时间。
- function CScor(el, tiers, parentScore){
- let score = 0;
- const tagName = el.tagName;
- // 判断当前的标签元素是否为指定的标签元素
- if(!filterTagNameInTagNames(tagName)){
- const childrenLen = el.children ? el.children.length : 0;
- // 判读子元素的长度是否大于0
- if(childrenLen>0){
- for(let childs = el.children, len = childrenLen-1; len >= 0; len--){
- score += calculateScore(childs[len],tiers+1,score>0)
- }
- }
- // 判断分数是否小于等于0,且父元素的分数为0
- if(score<= 0&& !parentScore){
- if(!(el.getBoundingClintRect&& el.getBoundingClintRect().top<WH)) return 0
- }
- score += 1 + 0.5 * tiers;
- }
- return score
- }
- function filterTagNameInTagNames(tagName){
- return ["SCRIPT","STYLE","META","HEAD"].some(tag=>tag===tagName)
- }
- calFinalScore(){
- try {
- if(this.sendMark) return;
- const time = Date.now() - window.performance.timing.fetchStart;
- 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 || (
- SCORE_ITEMS.length > 10 && window.performance.timing.loadEventEnd !== 0 &&
- SCORE_ITEMS[SCORE_ITEMS.length-1].score === SCORE_ITEMS[SCORE_ITEMS.length - 1].score
- );
- if(this.observer && isCheckFMP){
- this.observer.disconnect
- // 取FMP时间,默认是30001大于30s会自动被过滤
- this.fmp = record && record.t || 30001
- try {
- this.checkImgs(document.body)
- let max = Math.max(...this.imgs.map(element=>{
- if(/^(\/\/)/.test(element)) element = "https:" + element
- try {
- return window.performance.getEntriesByName(element)[0].responseEnd || 0
- } catch (error) {
- return 0
- }
- }))
- } catch (error) {
- return
- }
- }
- } catch (error) {
- return
- }
- }
如果页面里包括图片,使用上面的首屏指标采集方案,结果准确吗?答案是不准确的。上述的计算逻辑主要针对的是DOM元素而做的,图片加载过程是异步,图片容器(图片的DOM元素)和内容的加载是分开的,当容器加载出来时,内容还没出来,一定要确保内容加载出来,才算是首屏。
进行个归纳,通常计算首屏时间的方法有:
- 首屏模块标记法
- 统计首屏内加载最慢的图片
- 自定义首屏
首屏模块标签标记法
在首屏模块标签标记法中,首屏时间等于firstScreen - performance.timing.navigationStart;。但是在实际业务中,能够使用首屏模块标签标记法的情况比较少,大多数页面都需要通过接口拉取数据才能完整展示,因此我们会使用JavaScript 脚本来判断首屏页面内容加载情况。
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>首屏</title>
- <script type="text/javascript">
- window.pageStartTime = Date.now();
- </script>
- <link rel="stylesheet" href="common.css">
- <link rel="stylesheet" href="page.css">
- </head>
- <body>
- <!-- 首屏可见模块1 -->
- <div class="module-1"></div>
- <!-- 首屏可见模块2 -->
- <div class="module-2"></div>
- <script type="text/javascript">
- window.firstScreen = Date.now();
- </script>
- <!-- 首屏不可见模块3 -->
- <div class="module-3"></div>
- <!-- 首屏不可见模块4 -->
- <div class="module-4"></div>
- </body>
- </html>
统计首屏内图片完成加载的时间
在实际进行首屏加载中,加载最慢的资源文件是图片,对此我们可以将加载最慢的图片文件的时间作为首屏时间。这是因为在浏览器发起HTTP请求,在页面中建立TCP连接,但是每个页面所能建立的连接数又是有限的,使得并不能一次性将所有的图片都能进行下载和展示。
基于此种情况,我们可以在页面DOM树构建完成后去遍历首屏内所有的图片标签,并对每个图片标签的onload事件进行监听,从而计算得到所有图片中加载时间的最大值。这样就得到首屏时间=加载最慢的图片的时间点 - performance.timing.navigationStart。
自定义模块内容计算法
由于在统计首屏内遍历图片标签列表得到最大加载时间比较复杂,对此在业务中可以通过自定义模块内容,来简化计算首屏时间。如下面的做法:
忽略图片等资源加载情况,只考虑页面主要DOM
只考虑首屏的主要模块,而不是严格意义首屏线以上的所有内容
6参考文章
《前端性能优化方法与实践》
《前端优化-如何计算白屏和首屏时间》
7写在最后
本文主要介绍了首屏指标采集相关的内容,这种性能采集方案靠谱吗?当前的互联网大厂又在使用什么采集方案呢?就目前而言,上面介绍的是当前应用的最好的首屏指标采集方案,兼容了单页面应用和服务端模板的页面。
原文链接:https://mp.weixin.qq.com/s/6bf8kgzCt3akAiZJYiAFNw