前端性能監控

最近在作關於前端性能監控的功能,花了點時間研究了一下。先放一張經典圖:javascript

由於是原圖,有點大,要橫着拉了看,上面這些標註的屬性就是window.performance.timing下的屬性,裏面一些含義這邊列舉一下(參考MDN),默認都是毫秒數:前端

navigationStart: 表徵了從同一個瀏覽器上下文的上一個文檔卸載(unload)結束時的UNIX時間戳。若是沒有上一個文檔,這個值會和PerformanceTiming.fetchStart相同。java

unloadEventStart:表徵了unload事件拋出時的UNIX時間戳。若是沒有上一個文檔,or if the previous document, or one of the needed redirects, is not of the same origin, 這個值會返回0.git

unloadEventEnd:表徵了unload事件處理完成時的UNIX時間戳。若是沒有上一個文檔,or if the previous document, or one of the needed redirects, is not of the same origin, 這個值會返回0.github

redirectStart:表徵了第一個HTTP重定向開始時的UNIX時間戳。若是沒有重定向,或者重定向中的一個不一樣源,這個值會返回0.算法

redirectEnd:表徵了最後一個HTTP重定向完成時(也就是說是HTTP響應的最後一個比特直接被收到的時間)的UNIX時間戳。若是沒有重定向,或者重定向中的一個不一樣源,這個值會返回0.數組

fetchStart:表徵了瀏覽器準備好使用HTTP請求來獲取(fetch)文檔的UNIX時間戳。這個時間點會在檢查任何應用緩存以前。瀏覽器

domainLookupStart:表徵了域名查詢開始的UNIX時間戳。若是使用了持續鏈接(persistent connection),或者這個信息存儲到了緩存或者本地資源上,這個值將和 PerformanceTiming.fetchStart一致。緩存

domainLookupEnd:表徵了域名查詢結束的UNIX時間戳。若是使用了持續鏈接(persistent connection),或者這個信息存儲到了緩存或者本地資源上,這個值將和 PerformanceTiming.fetchStart一致。安全

connectStart:返回HTTP請求開始向服務器發送時的Unix毫秒時間戳。若是使用持久鏈接(persistent connection),則返回值等同於fetchStart屬性的值。

connectEnd:返回瀏覽器與服務器之間的鏈接創建時的Unix毫秒時間戳。若是創建的是持久鏈接,則返回值等同於fetchStart屬性的值。鏈接創建指的是全部握手和認證過程所有結束。

secureConnectionStart:返回瀏覽器與服務器開始安全連接的握手時的Unix毫秒時間戳。若是當前網頁不要求安全鏈接,則返回0。

requestStart:返回瀏覽器向服務器發出HTTP請求時(或開始讀取本地緩存時)的Unix毫秒時間戳。

responseStart:返回瀏覽器從服務器收到(或從本地緩存讀取)第一個字節時的Unix毫秒時間戳。若是傳輸層在開始請求以後失敗而且鏈接被重開,該屬性將會被數製成新的請求的相對應的發起時間。

responseEnd:返回瀏覽器從服務器收到(或從本地緩存讀取,或從本地資源讀取)最後一個字節時(若是在此以前HTTP鏈接已經關閉,則返回關閉時)的Unix毫秒時間戳。

domLoading:返回當前網頁DOM結構開始解析時(即Document.readyState屬性變爲「loading」、相應的readyStateChange事件觸發時)的Unix毫秒時間戳。

domInteractive:返回當前網頁DOM結構結束解析、開始加載內嵌資源時(即Document.readyState屬性變爲「interactive」、相應的readyStateChange事件觸發時)的Unix毫秒時間戳。

domContentLoadedEventStart:返回當解析器發送DOMContentLoaded事件,即全部須要被執行的腳本已經被解析時的Unix毫秒時間戳。

domContentLoadedEventEnd:返回當全部須要當即執行的腳本已經被執行(不論執行順序)時的Unix毫秒時間戳。

domComplete:返回當前文檔解析完成,即Document.readyState 變爲 'complete'且相對應的readyStateChange被觸發時的Unix毫秒時間戳。

loadEventStart:返回該文檔下,load事件被髮送時的Unix毫秒時間戳。若是這個事件還未被髮送,它的值將會是0。

loadEventEnd:返回當load事件結束,即加載事件完成時的Unix毫秒時間戳。若是這個事件還未被髮送,或者還沒有完成,它的值將會是0.

 

上面屬性比較多,可是着重要注意的點已經用紅色加粗標註出來了,其餘的時間節點不是不重要,而是可能咱們在監控前端性能的一些點的時候暫時不會用到。

下面是我在項目中用到的一些時間監控的算法:

const getPerformanceTiming = () => {  
    let performance = window.performance;
 
    if (!performance) {
        // 當前瀏覽器不支持
        console.log('你的瀏覽器不支持 performance 接口');
        return;
    }
 
    let t = performance.timing;
    let times = {};
 
    //【重要】頁面加載完成的時間
    times.onload = t.loadEventEnd - t.navigationStart;

    //【重要】解析DOM樹結構的時間,包括內嵌資源
    times.domResolved = t.domComplete - t.domLoading;
 
    //【重要】dom準備開始解析,從最開始到準備開始解析DOM的時間
    times.domReadyResolve = t.domLoading - t.navigationStart;
 
    //【重要】白屏時間,讀取頁面第一個字節的時間
    times.firstPaint = t.responseStart - t.navigationStart;
 
    //【重要】內容加載完成的時間
    times.request = t.responseEnd - t.requestStart;
 
    //【重要】time to interactive
    times.tti = t.domInteractive - t.requestStart;
 
    return times;
}

export default getPerformanceTiming 

上面我只標註了6個時間段,實際上能夠有更多,可是咱們公司只要上報部分時間,這邊我分享一個谷歌對前端頁面展現時間節點的規範:

上面這幾張圖,其實咱們在Chrome控制檯的Performance裏面也能截到,谷歌定義了四個節點,我這邊大體解釋一下:

FP:表示當第一個元素被渲染的時候,這爲首次節點,咱們能夠默認爲是第一個字節被讀取的時候,它就開始了,由於瀏覽器是一邊讀取一邊渲染的。

FCP:表示第一個內容節點被渲染的時候,多是某個文本,導航欄,svg圖片等。

FMP:表示第一個有意義的展現,也就是最大程度的頁面變化時,會算這個節點。

TTI:表示頁面從用戶角度變爲交互所需的時間,而不必定是當頁面正式完成加載時。頁面的初始JS被加載和主線程閒置的點(沒有長任務)。

其實還有FI(first interactive)和CI(completely interactive),前者是當全部必需的腳本已經加載而且CPU足夠空閒以處理大多數用戶輸入時,屏幕上的大多數(不必定是所有)UI元素都是交互式的;後者是一個比FI更全面的測量,它不只涵蓋了頁面上顯示的全部內容,並且頁面每50ms至少控制一次主線程,爲瀏覽器提供足夠的空間來處理流暢的輸入。總之,這是大多數網絡資源完成加載而且CPU長時間處於空閒狀態的時刻。

上面這些是谷歌定義的頁面須要追蹤的一些衡量標準,能夠供參考,具體的算法仍是看具體業務。

 

回到以前的定義的getPerformanceTiming函數,這個函數雖然能準確的拿到咱們想要的時間節點,可是存在一個問題,就是當咱們執行這個函數的時候,可能裏面某些時間節點還未取到,好比像domComplete和loadEventEnd這些,若是還未拿到的點,訪問就是0,因此咱們不能在初始化頁面的時候就去執行這個函數,什麼時候執行?

這邊個人想法是兩個:第一,設置一個定時器,不斷地去輪詢查看當前是否拿到了全部的值,有的話就把時間都算出來,沒有就繼續輪詢,輪詢間隔這個看我的,500也行,1000也行,可是這種方法有個缺點就是好比這個loadEventEnd,他必須頁面全部的資源所有請求完畢纔會有數值,若是你某個頁面的某個極小的圖片資源加載半天,就會致使你整個頁面全部的時間點都拿不到,而後沒辦法上報,那這確定是不行的,並且若是它加載了10s,你500毫秒一輪詢,那差很少就要輪詢20次,那這也是不必的,若是用戶以爲當前頁面已加載完成,而實際並未加載完成,此時他直接跳走了,那這個頁面就沒辦法上報了,等於浪費了,所以綜上狀況,就考慮了第二種方法,先上代碼:

document.onreadystatechange = function(){
    if (document.readyState === 'interactive') {
        setTimeout(function(){
            if (document.readyState === 'complete') {
                console.log(getPerformanceTiming());
            } else {
                console.log('time out');
            }
        }, 2000);
    }
};

 這裏我默認了從DOM結束解析的節點開始計時,若是2s以後沒有加載完成,那這個數據就不要了,默認爲髒數據,那必定是用戶那邊網絡出現了問題,(由於考慮到百分之九十都是1s左右,若是你頁面大概就要2s左右,那你的時間節點能夠設置久一些),若是加載完成,那就取到這些時間段,而後進行上報,這有個好處就是隻取一次,而且能夠有個time out的限制。

 

至於上報的函數,這裏分享一個新浪移動前端技術專家小爝(爝神)寫的一個上報模板,地址:https://github.com/xiaojue/fe-report

 

若是我想了解具體的某些資源的加載狀況怎麼辦,能夠經過window.performance.getEntries(),它會返回一個數組,裏面是當前頁面已經加載完成的全部資源組成的數組,注意,是已經加載完成的,若是此時某個資源還在pending,那麼是拿不到這個元素的,因此這個方法也不能當即調用,要麼放到window.onload事件裏,要麼定時輪詢去拿,當數組趨於穩定狀態的時候,就當作它所有完成了。下面貼一個該數組的大體樣子:

那裏面每一項元素展開的屬性列表有哪些:

其實能發現不少屬性跟performance.timing同名,含義也是如此,有一個duration用得比較多,它表示當前資源加載總時間,那麼咱們要獲取到全部資源請求時間,就循環遍歷該數組,而後取一個最大值,就是全部資源時長。

 

寫了這麼多,感受大功告成了?呵呵🙄,最嚴重的問題出現了,兼容性!!!直接來連接:https://caniuse.com/#search=performance

安卓用戶還好,4.4以上都支持,可是iOS就比較坑了,須要11.0版本以上,這已經算是比較高的了,因此針對這部分iOS用戶,有辦法解決嗎?

沒辦法。。。window.performance暫時沒有兼容性的解決方案。

爝神給了一個方案,針對不支持的用戶羣體,只能經過在頁面埋點的方法,好比在head裏的link標籤先後埋點,我雖然拿不到每一個資源的時間,可是我默認你link標籤加載完成就是執行完成,那我在加載先後算一下時間差,默認就是你的請求時間,還有就是在body的最後打一個點,添加一個script,當執行到最後一個js的時候,我默認你當前的靜態資源讀取完成。還有一些比較大的圖片,我能夠在具體的圖片裏面添加onload事件,而後算一下該圖片加載的時間,默認就是全部資源加載完成的時間。

 

以上的方法都是針對網頁或者普通移動h5的項目,可是若是是SPA應用怎麼辦,只有首屏加載的時候纔會觸發window.performance,後續的路由加載因爲是單頁面,因此不會再觸發window事件,這也是比較頭疼的問題,咱們公司項目移動端用的是React全家桶,因此我第一反應是能夠經過組件的生命週期來拿到一些關鍵時間點,好比DOM掛載等。這裏具體的就不說了,遇到一個坑就是HOC,也就是高階組件,不管是代理或者反向繼承,生命週期函數都會把傳入的組件的生命週期函數給覆蓋掉,mmp,我也是試過了才發現的。因此不能用這種hoc的方式。

 

對了,這裏補充一個關於React組件生命週期執行順序問題,我這邊本身也嘗試了一下:

<App>
	<Test1 />
	<Test2 />
</App>

像這樣的組件,執行順序如何呢?看下:

APP willMount
父組件render
子組件1willMount
子組件1render
子組件2willMount
子組件2render
子組件1didmount
子組件2didmount
父組件didMount

 對了,再多說一點,任何子組件didmount的時候,全部的真實DOM已經掛載完成,並且都是同時執行的,所以這裏是一個節點,能夠用來記錄DOM樹掛載完成。

 

end

相關文章
相關標籤/搜索