關於首屏
首屏時間是指從轉向該頁面到屏幕中該頁面所有內容都可見時的時間。已經有太多的關於首屏時間的計算,在本文中並不重復闡述這些已經被提出或者實現的方案,而旨在探索與討論更多的首屏自動化采集方案,擴大思考范圍,你我思想之間互相碰撞往往可以激起更多的稀奇古怪的解決方案,這也正是我寫這篇文章的目的。
通過瀏覽器調試工具,我們可以清晰的看出頁面資源加載時序圖:
先是html頁面加載,token進行詞法、語法解析后開始加載靜態資源並執行相關腳本,開始構建DOM樹、render樹和CSSOM數,最后加載圖片,用戶看到完整的網頁。
雖然瀏覽器有着各自的優化的解決方案,但是大多數情況下圖片往往是最后加載完畢,這不僅僅是由於圖片的大小相對較大,而且圖片的加載與否與DOM結構有着很大的關系。DOM是否構建完畢,render樹中是否渲染以及其他的圖片加載策略有關系可能都會影響圖片加載時序。因此在首屏時間的計算中,我們是以最終首屏圖片的加載時間為節點計算的。
首屏計算
原則1 首屏計算模塊不應該耦合業務線
一般而言,首屏計算作為一個抽離出的js腳本單獨引用,這個模塊盡量不暴露API給開發者使用,所有的采集端任務都由該模塊完成。這句話可能聽起來像一句廢話,但還是有很多情況可能需要業務人員來進行首屏渲染時間的判斷的,下面將針對這個情形舉一個實際的場景:
隨着MVVM模式的興起,前端異步渲染逐漸流行起來,前端編碼逐漸由面向jQuery編程轉向為面向Vue編程。可是使用Vue編寫的業務代碼在本地打包后僅僅是一個bundle,此時的HTML文件中只是一個
的占位符而已,那么首屏時間計算模塊該如何准確的計算首屏時間呢? 因此首屏時間計算模塊必須知道首屏的DOM結構渲染完畢的時間節點,在這個節點時刻進行計算首屏范圍內的圖片加載時間。 可是如何獲取首屏DOM結構渲染完畢的時間節點呢?這就需要業務開發人員制定。在更新vue實例的data屬性后,通知首屏計算模塊此時DOM接口已渲染完畢,開始計算首屏時間。MVVM開發模式下,首屏時間的計算已經耦合了業務代碼,雖然可以在保證首屏時間的准確性,但卻給開發者帶來了一些可觀判斷邏輯,而這些判斷往往會困擾新入職的同志們,因此我們的目標之一就是解決需要手動打點進行首屏時間計算的現狀。
原則2 性能與准確性的權衡
業界有個通過canvas截屏並通過輪詢對比不同時間點截屏圖片之間某幾個隨機像素點,從而判斷首屏是否加載完畢。這種方式雖然科學,但是估計沒有幾個公司會采用這種方案。通過canvas截屏這個操作對硬件的要求可能就比較高,而且需要進行額外的像素運算,因此性能肯定很差。其實這種場景在工程領域經常出現,工程不同於科學那般嚴謹,我們只需要找到給定條件的最優解即可,做工程也就是在做trade off。因此這種對比方案我們也必須摒棄。
實現
再次強調,由開發者打點首屏DOM渲染完畢進行首屏時間計算的方式是相對准確的方式,因此我們后續討論的自動化計算首屏時間的准確性都是基於此標准進行對比說明,因為自動化計算肯定是沒有人工干預准確的,這一點毫無疑問。
輪訓采集大法
仍然是輪訓,不同的是在每次輪詢中執行一些操作:
- 獲取首屏的所有圖片(包括IMG標簽與css相關屬性)
- 綁定首屏圖片的onload和onerror事件,每次輪詢不會重復綁定已綁定的圖片
- 相同圖片不需重復綁定事件偵聽,否則會與 2 中的每次輪詢混淆
- 圖片的事件處理函數執行打點信息並統計圖片加載狀態,同時比對時間戳得到最遲加載的時間
具體的實現中,需要特別注意首屏出現的相同圖片的情況。筆者起初在獲取首屏圖片中簡單計算圖片的url數組,存儲重復圖片的個數,並且與該圖片的加載狀態綁定在一起。如首屏中出現了3張相同的圖片,那么在該圖片onload或onerror中對已加載圖片的數量做 加3 處理,否則導致最終的 已加載圖片總數 與 首屏圖片總數 不相等的情況發生。這種實現導致邏輯非常的差,且實現復雜。后通過存儲圖片所在的DOM對象數組實現更為簡單的圖片狀態判斷,更加已讀。
偽代碼如下:
// totalCounter為輪詢的總時間
// DemandCounter是規定的輪詢總時間,為3000ms
// imgsLoadedCount則為首屏已加載的圖片數量
// lastImageLoadedStamp為最后加載的圖片時間戳
function checkFirstScreenDomReady(){
if(totalCounter >= DemandCounter){
// ...
var stamps = Object.keys(pools),
len = stamps.length,
i = 0,
it;
finalImgCount = pools[stamps[len - 1]].imgLen;
pollEnd = true;
for(;i<len;i++){
it = pools[stamps[i]];
if(it.imgLen == finalImgCount && it.imgsLoadedCount >= finalImgCount){
self.onRecord = true;
_perfQueue._firstScreenLoadEnd = lastImageLoadedStamp;
firstScreen.firstScreenLoadEnd = lastImageLoadedStamp;
firstScreen.duaring = lastImageLoadedStamp - performance.timing.navigationStart;
reportData(firstScreen);
return;
}
}
return;
}
var imgEls = getImage();
imgEls.forEach(function(el) {
if(!imgLoadedHash.get(el)){
var img = new Image();
imgLoadedHash.put(el,{
loaded: true,
});
img.onload = OnLoad;
img.onerror = OnError;
img.src = el.__src;
}
});
pools[totalCounter+''] = {
imgLen: imgEls.length,
stamp: Date.now(),
imgsLoadedCount: imgsLoadedCount
};
totalCounter += timeout;
}
watch dog采集
利用Mutation Observer API進行偵聽 內容框的DOM事件,判斷首屏DOM結構是否完備;如果構建完畢則偵聽首屏范圍內的圖片加載事件,計算首屏時間。
watch dog需要知曉合適首屏DOM構建完畢。這需要首屏計算模塊主動插入一個打點標簽
,將業務代碼放置在標簽內部(這個步驟最好放在發布階段,由腳手架操作)。通過mutation 偵聽 .j_collector_container 容器的DOM子孫節點變化。如在observe事件處理函數中,計算 .j_collector_container 高度,如果大於屏幕高度則意味着首屏的DOM結構已渲染完畢,開始計算首屏時間。在計算 .j_collector_container 高度時,最好采用限流策略,防止短時間內計算多次容器的布局信息,這也是無可奈何之舉。
此處的偽代碼如下:
// 記錄首屏DOM元素的位置信息
var firstScreenDomReady = false;
var callback = function(records){
if(firstScreenDomReady)
return;
// 此處需做throttle 處理
for(var i=0,len=records.length;i<len;i++){
// 判斷首屏DOM渲染完畢的策略:
// 判斷collectWrapper元素高度是否大於首屏
var cr = collectWrapper.getBoundingClientRect(),
screenHeight = win.innerHeight;
if(cr.top + cr.height >= screenHeight){
firstScreenDomReady = true;
recordFirstScreenLoad();
return;
}
}
};
var mo = new MutationObserver(callback);
var option = {
'childList': true,
'subtree': true
};
var collectWrapper = document.querySelector('.j_collector_wrapper');
if(collectWrapper.getBoundingClientRect().height < win.innerHeight){
mo.observe(collectWrapper, option);
}else{
setTimeout(function(){
recordFirstScreenLoad();
});
}
總結
不管采用哪種方式,計算出來的首屏時間都不是准確的。而且在每種實現中都需要通過JS引擎與渲染引擎的bridge進行通信執行耗時的操作,如getBoundingClientRect和訪問offsetTop屬性導致relayout。不過這也是沒有辦法的辦法,在瀏覽器不提供相關首屏API的前提下我們只有這么做。
另外,對比這三種實現(開發者手動打點、輪訓、watch dog采集),針對一個復雜的電商首屏做了性能測試,該頁面首屏部分有7個非常復雜的子組件,得到如下結果:
結果也符合我們的預期。
我的博客即將搬運同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan