做移动web页面,受移动网络网速和终端性能影响,我们经常要关注首屏内容展示时间(以下简称首屏时间)这个指标,它衡量着我们的页面是否能在用户耐心消磨完之前展示出来,很大程度影响着用户的使用满意度。
首屏时间的定义
工信部在《宽带速率的测试方法用户上网体验》规范标准中对首屏时间的定义为:
浏览器显示第一屏页面所消耗的时间,以800x600像素尺寸为标准,从开始加载到浏览器页面显示高度达到600像素且此区域有内容显示的时间。
也就是说用户能够看到区域内所有元素加载完的时间。
一个页面的“总加载时间”要比“首屏时间”长,但对于最终用户体验而言,当内容充满首屏的区域时,用户就可以看到网站的主要内容并可以进行各自的选择了。首屏时间的快与慢,直接影响到了用户对网站的认知度。
在国内的网络条件下,通常一个网站,如果“首屏时间”在2秒以内是比较优秀的,5秒以内用户可以接受,10秒以上就不可容忍了。
对于页面的加载时间,dom都提供了api接口,比如,整个页面dom树的构建时间,我们通过打点DOMContentLoaded,就能得到。整个页面加载完成,包括图片,视频等外部资源,我们通过window.onload的方法也可以得到。但是对于首屏,没有这样的接口提供。
那么如何去计算首屏的加载时间?
首先要解决的问题是哪些属于首屏的内容。由于手机屏幕尺寸的多样性,同一页面在手机屏幕上用户所能看到的首屏内容有可能不一样。所以需要去判断哪些元素属于首屏元素并且该元素加载是否完成。对于非可替换元素,dom的加载完成说明了该元素已经完成加载,而对于一些可替换元素,如img标签,需要外部资源的加载完成才能有实际内容的展示,而页面耗时最大的部分也是这些外部资源的加载。
因为通常需要考虑首屏时间的页面,都是因为在首屏位置内放入了较多的图片资源。现代浏览器处理图片资源时是异步的,会先将图片长宽应用于页面排版,然后随着收到图片数据由上至下绘制显示的。并且浏览器对每个页面的TCP连接数限制,使得并不是所有图片都能立刻开始下载和显示。因此我们在DOM树构建完成后即可遍历获得所有在设备屏幕高度内的所有图片资源标签,在所有图片标签中添加document.onload事件,在整页加载完成(window.onLoad事件发生)时遍历图片标签并获得之前注册的document.onload事件时间的最大值,该最大值减去navigationStart即认为近似的首屏时间。而对于页面没有图片的页面,我们可以近似认为首屏的加载时间为dom完成的时间。
function firstScreen() { //收集所有页面的加载时间 var imgs = document.getElementsByTagName("img"); var fsItems = []; var loadEvent = function() { //gif避免 if (this.removeEventListener) { this.removeEventListener("load", loadEvent, false); } var curTime = +new Date; fsItems.push({ img: this, time: curTime }); } for (var i = 0; i < imgs.length; i++) { (function() { var img = imgs[i]; if (img.addEventListener) { !img.complete && img.addEventListener("load", loadEvent, false); } else if (img.attachEvent) { img.attachEvent("onreadystatechange", function() { if (img.readyState == "complete") { loadEvent.call(img, loadEvent); } }); } })(); } //获取元素在dom中的位置 function getOffsetTop(elem) { var top = 0; top = window.pageYOffset ? window.pageYOffset : document.documentElement.scrollTop; top += elem.getBoundingClientRect().top; return top; } function findMaxTime() { var sh = document.documentElement.clientHeight, maxTime = 0; for (var i = 0; i < fsItems.length; i++) { var item = fsItems[i], img = item['img'], time = item['time'], top = getOffsetTop(img); if (top > 0 && top < sh) { //找首屏中的图片 maxTime = time > maxTime ? time : maxTime; } } return maxTime; } window.addEventListener('load', function() { var imgTime = findMaxTime(), domTime = window.performance.timing.domInteractive, //dom完成时间 speedTime, startTime = window.performance.timing.navigationStart || window.performance.timing.startTime, //页面首页时间 screenTime = imgTime > 0 ? imgTime : domTime; //如果没有图片,直接取dom时间 speedTime = screenTime - startTime; console.log(speedTime); }); } firstScreen();
这里需要注意一点,需要在页面整个onload完成之后再去计算首屏,因为可替换元素如果没有固定好高度有可能会导致在渲染过程中页面重排。
以上就是通过首屏高度图片加载的办法实现的统计,这种方法能够解决对于首屏内容在服务端生成的情况,有时候我们首屏数据需要通过异步请求获得,这种方式就不适合了。那么这类数据又有什么解决的办法。
首先,我们的数据异步请求,页面的渲染依赖于数据接口,我们可以在首屏的接口返回时间打点。这种做法能够统计出时间,但是对于业务逻辑的代码依赖性强,不可能抽象出普用性的api接口。
这里提出另外一种方法:图像相似度比较法,通过比较连续截屏图像的像素点变化趋势确定首屏时间,这里有个问题,通过连续比对的方法,我们只能得出首屏加载完成的时间区间段,同时这种截图的方法会比较消耗本地设备的运行资源。代码方法实现如下:
function firstScreen() { var winWidth = document.documentElement.clientWidth, winHeight = document.documentElement.clientHeight, nineDots = findScreenDot(), preData = 0, funStartTime, timer, runTime; printScreen(); //找首屏上的九个点 function findScreenDot() { var sw = Math.ceil(winWidth / 6), sh = Math.ceil(winHeight / 6), dotArry = []; for (var i = 0; i < 3; ++i) { for (var j = 0; j < 3; ++j) { dotArry.push([sw * (i * 2 + 1), sh * (j * 2 + 1)]) } } return dotArry; } funStarTime = +new Date(); printScreen(); //计算时间 function calculateTime(time) { var startTime = window.performance.timing.navigationStart || window.performance.timing.startTime; //页面首页时间 console.log(time - startTime - runTime); } //截图 function printScreen() { funStartTime = +new Date(); html2canvas(document.body).then(function(canvas) { var context = canvas.getContext('2d'); var imageData = context.getImageData(0, 0, winWidth, winHeight); //截图初始化费时 if (preData == 0) { runTime = +new Date() - funStartTime; } var colorTotal = 0; //对9个点的颜色红色通道像素值求值 for (var i = 0, length = nineDots.length; i < length; ++i) { colorTotal += imageData.data[((nineDots[i][0] * (imageData.width * 4)) + (nineDots[i][1] * 4))] } //前后数据相同,可以上报时间 if (preData == colorTotal && preData != 0) { calculateTime(+new Date()) clearTimeout(timer); } else { preData = colorTotal; timer = setTimeout(function() { printScreen(); }, 100) } }); } } firstScreen();
这里采用了html2canvas插件,每100ms截取屏幕的,然后获取屏幕九宫格每一格中心点的,获取红色通道的像素相加得到一个值,通过不断截屏和比较这个求和的值,监控出首屏是否加载完毕。
通过对以上两种方法的比对,截屏图像相似度比较的方法最为科学和直观,但是比较消耗本地设备的运行资源。而且由于比较复杂的运算,会影响到页面逻辑脚本执行的性能。所以在首屏测试的时候,推荐使用首屏高度内图片加载的方法,计算它们加载完的时间去得到首屏时间,这样比较符合网页的实际体验并且比较节省设备运行资源。