做移動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截取屏幕的,然后獲取屏幕九宮格每一格中心點的,獲取紅色通道的像素相加得到一個值,通過不斷截屏和比較這個求和的值,監控出首屏是否加載完畢。
通過對以上兩種方法的比對,截屏圖像相似度比較的方法最為科學和直觀,但是比較消耗本地設備的運行資源。而且由於比較復雜的運算,會影響到頁面邏輯腳本執行的性能。所以在首屏測試的時候,推薦使用首屏高度內圖片加載的方法,計算它們加載完的時間去得到首屏時間,這樣比較符合網頁的實際體驗並且比較節省設備運行資源。