前端性能監控


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

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

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

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

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM