首屏時間是指從轉向該頁面到屏幕中該頁面全部內容均可見時的時間。已經有太多的關於首屏時間的計算,在本文中並不重複闡述這些已經被提出或者實現的方案,而旨在探索與討論更多的首屏自動化採集方案,擴大思考範圍,你我思想之間互相碰撞每每能夠激起更多的稀奇古怪的解決方案,這也正是我寫這篇文章的目的。css
經過瀏覽器調試工具,咱們能夠清晰的看出頁面資源加載時序圖:html
先是html頁面加載,token進行詞法、語法解析後開始加載靜態資源並執行相關腳本,開始構建DOM樹、render樹和CSSOM數,最後加載圖片,用戶看到完整的網頁。前端
雖然瀏覽器有着各自的優化的解決方案,可是大多數狀況下圖片每每是最後加載完畢,這不只僅是因爲圖片的大小相對較大,並且圖片的加載與否與DOM結構有着很大的關係。DOM是否構建完畢,render樹中是否渲染以及其餘的圖片加載策略有關係可能都會影響圖片加載時序。所以在首屏時間的計算中,咱們是以最終首屏圖片的加載時間爲節點計算的。vue
通常而言,首屏計算做爲一個抽離出的js腳本單獨引用,這個模塊儘可能不暴露API給開發者使用,全部的採集端任務都由該模塊完成。這句話可能聽起來像一句廢話,但仍是有不少狀況可能須要業務人員來進行首屏渲染時間的判斷的,下面將針對這個情形舉一個實際的場景:編程
隨着MVVM模式的興起,前端異步渲染逐漸流行起來,前端編碼逐漸由面向jQuery編程轉向爲面向Vue編程。但是使用Vue編寫的業務代碼在本地打包後僅僅是一個bundle,此時的HTML文件中只是一個 canvas
的佔位符而已,那麼首屏時間計算模塊該如何準確的計算首屏時間呢? 所以首屏時間計算模塊必須知道首屏的DOM結構渲染完畢的時間節點,在這個節點時刻進行計算首屏範圍內的圖片加載時間。 但是如何獲取首屏DOM結構渲染完畢的時間節點呢?這就須要業務開發人員制定。在更新vue實例的data屬性後,通知首屏計算模塊此時DOM接口已渲染完畢,開始計算首屏時間。MVVM開發模式下,首屏時間的計算已經耦合了業務代碼,雖然能夠在保證首屏時間的準確性,但卻給開發者帶來了一些可觀判斷邏輯,而這些判斷每每會困擾新入職的同志們,所以咱們的目標之一就是解決須要手動打點進行首屏時間計算的現狀。數組
業界有個經過canvas截屏並經過輪詢對比不一樣時間點截屏圖片之間某幾個隨機像素點,從而判斷首屏是否加載完畢。這種方式雖然科學,可是估計沒有幾個公司會採用這種方案。經過canvas截屏這個操做對硬件的要求可能就比較高,並且須要進行額外的像素運算,所以性能確定不好。其實這種場景在工程領域常常出現,工程不一樣於科學那般嚴謹,咱們只須要找到給定條件的最優解便可,作工程也就是在作trade off。所以這種對比方案咱們也必須摒棄。瀏覽器
再次強調,由開發者打點首屏DOM渲染完畢進行首屏時間計算的方式是相對準確的方式,所以咱們後續討論的自動化計算首屏時間的準確性都是基於此標準進行對比說明,由於自動化計算確定是沒有人工干預準確的,這一點毫無疑問。app
仍然是輪訓,不一樣的是在每次輪詢中執行一些操做:異步
具體的實現中,須要特別注意首屏出現的相同圖片的狀況。筆者起初在獲取首屏圖片中簡單計算圖片的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; }
利用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