10

统计页面首屏时间,很多人第一步就错了

 4 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzU0OTExNzYwNg%3D%3D&%3Bmid=2247489140&%3Bidx=1&%3Bsn=28ae4578909cdf69e6a4476de5cde01b
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

2umu2eQ.jpg!mobile

前言

前端页面性能对用户留存、用户直观体验有着重要作用。这样的话如何更好的监控前端页面性能就变的十分重要。前端页面的性能监控主要分为两个方式:

一种叫做 合成监控 Synthetic Monitoring, SYN 。就是在一个模拟场景里,提交一个需要做性能审计的页面,通过一系列的工具、规则去运行页面,提取一些性能指标,得出一个审计报告。合成监控中最近比较流行的是 GoogleLighthouse

另一种是 真实用户监控 Real User Monitoring,RUM 。监控真实的用户访问数据,上报数据到服务器,然后经过数据清洗加工,得到最终的性能数据。

在前端性能监控中有一个非常重要的指标就是首屏时间,因为首屏时间直接反应了用户多久能看到页面的主要内容,这决定了用户体验。这样的话,如何取到准确的首屏时间对我们来说就变的非常重要。本文就结合之前的实践,聊一聊首屏时间如何计算。

Performance

在 SSR(服务端渲染)的应用中,我们认为 htmlbody 渲染完成的时间就是首屏时间。我们通常使用 W3C 标准的 Performance 对象来计算首屏时间。

Performance 经常被用于采集性能数据,因为对象内置了几乎所有常用前端需要的性能参数。

jAVJbam.png!mobileimage

Performance 包含了四个属性: memorynavigationtimeOrigintiming ,以及一个事件处理程序 onresourcetimingbufferfull 。下面我们简单介绍一下 Performanceapi

memory

memory 这个属性提供了一个可以获取到基本内存使用情况的对象 MemoryInfo

performance.memory = {
jsHeapSizeLimit, // 内存大小限制,单位是字节B
totalJSHeapSize, // 可使用的内存大小,单位是字节B
usedJSHeapSize // JS对象占用的内存大小,单位是字节B
}

navigation

返回 PerformanceNavigation 对象,提供了在指定的时间段发生的操作相关信息,包括页面是加载还是刷新、发生了多少重定向等。

performance.navigation = {
redirectCount: '',
type: ''
}

timeOrigin

返回性能测量开始的时间的高精度时间戳

timing

返回 PerformanceTiming 对象,包含了各种与浏览器性能相关的数据,提供了浏览器处理页面的各个阶段的耗时。下面是常用时间点计算

window.onload = function() {
var timing = performance.timing;
console.log('准备新页面时间耗时: ' + timing.fetchStart - timing.navigationStart);
console.log('redirect 重定向耗时: ' + timing.redirectEnd - timing.redirectStart);
console.log('Appcache 耗时: ' + timing.domainLookupStart - timing.fetchStart);
console.log('unload 前文档耗时: ' + timing.unloadEventEnd - timing.unloadEventStart);
console.log('DNS 查询耗时: ' + timing.domainLookupEnd - timing.domainLookupStart);
console.log('TCP连接耗时: ' + timing.connectEnd - timing.connectStart);
console.log('request请求耗时: ' + timing.responseEnd - timing.requestStart);
console.log('白屏时间: ' + timing.responseStart - timing.navigationStart);
console.log('请求完毕至DOM加载: ' + timing.domInteractive - timing.responseEnd);
console.log('解释dom树耗时: ' + timing.domComplete - timing.domInteractive);
console.log('从开始至load总耗时: ' + timing.loadEventEnd - timing.navigationStart);
}

通过上面的介绍, 我们可以轻松的得到首屏时间

domLoadedTime = timing.domContentLoadedEventStart - timing.navigationStart

FMP

但是随着 VueReact 等前端框盛行, 导致 Performance 无法准确的监控到页面的首屏时间。因为页面的 body 是空,浏览器需要先加载 js , 然后再通过 js 来渲染页面内容。那我们使用什么数据来当做首屏时间呢?

Lighthouse 中我们可以得到 FMP 值,FMP(全称 First Meaningful Paint,翻译为首次有效绘制)表示页面的主要内容开始出现在屏幕上的时间点,它是我们测量用户加载体验的主要指标。我们可以认为 FMP 的值就是首屏时间,但是浏览器并没有把 FMP 的数据提供出来。那我们如何计算呢?

整个计算流程分为两个下面两个部分:1、监听元素加载,主要是为了计算 Dom 的分数 2、计算分数的曲率,计算出最终的 FMP

初始化监听

initObserver() {
try {
if (this.supportTiming()) {
this.observer = new MutationObserver(() => {
let time = Date.now() - performance.timing.fetchStart;
let bodyTarget = document.body;
if (bodyTarget) {
let score = 0;
score += calculateScore(bodyTarget, 1, false);
SCORE_ITEMS.push({
score,
t: time
});
} else {
SCORE_ITEMS.push({
score: 0,
t: time
});
}
});
}

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

if (document.readyState === "complete") {
this.mark = 'readyState';
this.calFinallScore();
} else {
window.addEventListener(
"load",
() => {
this.mark = 'load';
this.calFinallScore();
},
true
);
window.addEventListener(
'beforeunload',
() => {
this.mark = 'beforeunload';
this.calFinallScore();
},
true
)
const that = this;
function listenTouchstart() {
if(Date.now() > 2000) {
that.calFinallScore();
this.mark = 'touch';
window.removeEventListener('touchstart', listenTouchstart, true);
}
}
window.addEventListener(
'touchstart',
listenTouchstart,
true
)
}
} catch (error) {}
}

我们通过 MutationObserver 来监听 Dom 的变化, 然后计算当前时刻 Dom 的分数。有人可能会问,如果 Dom 每一次变化,都进行监听,是不是会特别消耗页面的性能?其实 MutationObserver 在执行回调时是批量执行,有些类似 Vue 等前端框架的渲染过程。

计算分数

function calculateScore(el, tiers, parentScore) {
try {
let score = 0;
const tagName = el.tagName;
if ("SCRIPT" !== tagName && "STYLE" !== tagName && "META" !== tagName && "HEAD" !== tagName) {
const childrenLen = el.children ? el.children.length : 0;
if (childrenLen > 0) for (let childs = el.children, len = childrenLen - 1; len >= 0; len--) {
score += calculateScore(childs[len], tiers + 1, score > 0);
}
if (score <= 0 && !parentScore) {
if (!(el.getBoundingClientRect && el.getBoundingClientRect().top < WH)) return 0;
}
score += 1 + .5 * tiers;
}
return score;
} catch (error) {

}
}

通过上面的代码,我们可以得到计算分数的步骤

1、从 body 元素开发递归计算 2、会排查无用的元素标签比较 SCRIPT 等 3、如果元素超出屏幕就认为是 0 分 4、第一层的元素是 1 分,第二次的元素是 1 + (层数 * 0.5),也就是 1.5 分,依次类推,最终得打整个 Dom 数的总体分数

计算出 FMP

我们通过 MutationObserver 得到了一个数组,数组的每一项就是每次 Dom 变化的时间和分数。那么我们怎么计算出想要的 FMP 的值呢?

let fmps = getFmp(SCORE_ITEMS);
let record = null
for (let o = 1; o < fmps.length; o++) {
if (fmps[o].t >= fmps[o - 1].t) {
let l = fmps[o].score - fmps[o - 1].score;
(!record || record.rate <= l) && (record = {
t: fmps[o].t,
rate: l
});
}
}

通过上面的代码,我们会得到最终的 FMP 的值,就是变化最大的这个 DOM 变化。

y26Brub.png!mobileimage

总结

到这里我们就基本把首屏时间的计算方式介绍完毕。总结为一句话,就是 SSR 使用 Dom 渲染结束的时间, SPA 的项目使用 FMP 的时间。

本月文章预告

预告下,接下来我们会陆续发布转转在多端 SDK、移动端等基础架构和中台技术相关的实践与思考,欢迎大家关注公众号 “大转转 FE”,期望与大家多多交流

YFNr6bv.jpg!mobile


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK