如何統計首屏渲染時間

倉庫完整代碼:first-screen-paint,若是來過,期待留下你的一顆小星星~css

認識幾個概念

1. First Paint(FP)node

First Paint的定義是渲染樹首次轉變爲屏幕像素的過程,咱們用FP time來表達首次渲染時間。在FP以前咱們看見的屏幕是空白的,那麼FP time也可理解爲白屏時間。如何計算呢?git

if (window.performance) {
    let pf = window.performance;
    let pfEntries = pf.getEntriesByType('paint')
    let fp = pfEntries.find(each => each.name === 'first-paint')
    console.log('first paint time: ', fp && fp.startTime)
}
複製代碼

2. First Contentful Paint(FCP):github

FCP定義的是從頁面加載到屏幕上首次有渲染內容的過程,這裏的內容能夠是文本、圖像、svg元素和非白色canvas元素。在下圖加載時間線中,圖二是FCP的時間點: image.png 咱們用FCP time來表達內容首次渲染時間。如何計算呢?web

if (window.performance) {
    let pf = window.performance;
    let pfEntries = pf.getEntriesByType('paint')
    let fp = pfEntries.find(each => each.name === 'first-contentful-paint')
    console.log('first paint time: ', fp && fp.startTime)
}
複製代碼

須要區別於FP,總有FP time ≤ FCP timecanvas

3. First Meaningful Paint(FMP)api

FMP定義的是從頁面開始加載到渲染出主要內容的過程,這個「主要內容」的定義依賴於各瀏覽器中的實現細節,所以它並無做爲一個標準化的指標。在Chrome的Lighthouse面板中咱們能夠看到這個指標: image.png瀏覽器

4. Largest Contentful Paint(LCP)markdown

FMP的範圍很差界定,但LCP的範圍是恆定的,它定義的是頁面開始加載到渲染出(視口內)最大內容(文本或圖像等)的過程。以下圖加載時間線: image.pngapp

image.png 第一個示例中,Instagram logo是視口中的最大內容,第二個示例中,綠色的文本是視口中的最大內容塊。咱們用LCP time表達最大內容渲染時間,如何計算呢?

new PerformanceObserver(list => {
    let entries = list.getEntriesByType('largest-contentful-paint');
    entries.forEach(item => {
        console.log('largest contentful pain time: ', item.startTime)
    })
}).observe({ entryTypes: ['largest-contentful-paint'] });
複製代碼

什麼是首屏渲染?

咱們這裏定義的首屏是指頁面無滾動的狀況下,從開始加載到視窗第一屏內容渲染完成的過程,遵循上面幾個概念的定義,咱們能夠稱它爲 last contentful paint,亦或first screen paint更貼切一些。在本文,咱們就把首屏渲染時間叫作first screen paint time(FSP time),要如何來統計呢?

統計首屏渲染時間

先考慮最簡單的場景:咱們的頁面是純靜態文本型的,即首屏裏面沒有圖片,內容是靜態文本。

咱們要先解決一個問題:如何界定哪些元素是屬於屏內的?

1. getBoundingClientRect

getBoundingClientRect用於獲取某個元素相對於視窗的位置,理論上咱們只要計算每個元素的位置信息,結合視窗的高度信息,咱們就能判斷元素是否屬於屏內。

但在真實狀況下,一個頁面dom的數量是很龐大的,大量的dom操做自己就會影響整個頁面的性能!況且,getBoundingClientRect會引發頁面重排(what forces reflow/layout),這並非一個理想的方案;

2. IntersectionObserver + MutationObserver

IntersectionObserver經過啓動一個觀察器,以一種異步的方式檢查目標元素是否出現於視窗(viewport)中,它返回的數據裏面包含了兩個重要的信息:

  • time:元素可見性發生變化的時間,一個高精度時間戳,單位毫秒;
  • intersectionRatio:目標元素的可見比例,介於0.0-1.0,爲0時表示元素不可見,爲1時表示元素徹底可見。

接下來咱們須要給每個元素添加一個intersection觀察器,MutationObserver能夠幫助咱們,它提供了監視dom樹變動的能力,咱們使用它監視document根節點的子樹的變化,爲新增的每個子節點註冊一個IntersectionObserver,參考以下代碼:

// 註冊可視性監聽器
const isObserver = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
        // 屏內元素
        if (entry.intersectionRatio > 0) {
            // 記錄節點及其時間,這裏也可使用人工打點的方式:performance.now()
            console.log(`${entry.target}: ${entry.time}`);
        }
    });
});

// 註冊DOM樹變動監聽器
const muObserver = new MutationObserver((mutations) => {
    if (!mutations) return;
    mutations.forEach((mu) => {
        if (!mu.addedNodes || !mu.addedNodes.length) return;
        mu.addedNodes.forEach((ele) => {
            // 只對元素節點進行監聽
            if (ele.nodeType === 1) {
                // 添加可視性變化監聽器
                isObserver.observe(ele);
            }
        });
    });
});

// 監聽document的子樹變化
muObserver.observe(document, {
    childList: true,
    subtree: true
});
複製代碼

更完整的代碼參考:first-screen-paint

場景2:首屏包含圖片資源,多是圖片元素或背景,須要計算加載最慢那張圖片資源的耗時

問題1:圖片資源是異步加載的,如何獲取資源的請求耗時?

前文咱們介紹了獲取LCP time的方法,用相似的方式,咱們也能獲取圖片資源的耗時,使用PerformanceObserver api監聽資源的加載耗時,它返回的數據裏面包含了幾個重要的信息:

  • name:資源URL;
  • initiatorType:資源類型,取值多是css|img|xmlhttprequest等;
  • startTime:請求開始時間,高精度時間戳值,單位毫秒;
  • responseEnd:請求響應返回的時間,高精度時間戳值,單位毫秒;
  • duration:responseEndstartTime的差值;
const pfObserver = new PerformanceObserver((list) => {
    const entries = list.getEntriesByType('resource');
    entries.forEach((item) => {
        // 各類資源的耗時
        // 首屏圖片資源白名單:imgUrlWhiteList = []
        console.log(`${item.name: ${item.duration}}`);
    });
});
// 設定性能監聽類別:資源
pfObserver.observe({ entryTypes: ['resource'] });
複製代碼

問題2:上面代碼中咱們監聽了全部資源的請求,如何取出首屏的圖片資源請求?

  1. 對於img標籤的圖片資源,咱們能夠在MutationObserver或者IntersectionObserver監聽器中直接操做dom讀取imgsrc或者data-src屬性,把圖片URL保存起來;
  2. 針對背景圖片,咱們使用getComputedStyle方法獲取節點的樣式表,並取出其background-image的值;

場景3:首屏內容是動態fetch的,甚至fetch的是圖片資源,就如商城首頁?

數據是動態fetch的,若是是純文本數據,無圖片資源。咱們的DOM樹變動監聽器能夠監聽到數據返回以後的渲染狀況,渲染過程會收集這些節點的可見性變化時間(這個時間確定是在fetch數據返回時間點以後的);若是渲染的是圖片資源,那麼就進入了上一個處理圖片資源的場景。

兩個問題

1. 首屏內容還在加載中,用戶觸發了頁面滾動?

頁面滾動以後,第二屏的內容就會出如今視窗,本來屬於首屏的內容(部份內容可能並未完成渲染)卻沒在視窗中。那麼,按照如上的統計方式,就會統計到當前處於視窗內容的渲染時間,這可能就是一個「偏差」。

咱們須要一個共識在首屏內容徹底渲染以前頁面觸發了滾動,說明頁面已是一個可交互的狀態,這種狀況下,咱們認爲,用戶觸發滾動時那一幀的內容,已是用戶和開發者雙方都能接受的首屏內容。基於這個前提,咱們的處理方式是:

在頁面滾動時,加一個鎖,中止監聽後續內容的變動,以初次滾動的時間點爲時間界線,統計在此時間點前發出的(依據startTime)全部資源的請求耗時和dom樹節點的渲染時間;

2. 在場景3下,首屏內容未加載完,用戶觸發了頁面滾動?

  • 這種狀況下只能保底統計到fetch請求的響應結束時間;
  • 若是用戶在響應以前觸發了滾動,這時候數據渲染還沒有開始,咱們的程序沒法捕捉到dom節點,那麼也拿不到響應的圖片資源,也就沒法統計後續的渲染時間;
  • 若是用戶是在數據返回以後,圖片資源渲染以前觸發的滾動,這種狀況下因爲可以捕捉dom樹節點的渲染,理論上咱們也可以獲取響應圖片資源的加載耗時;

測試一下

在本測試demo中,頁面的主體內容是img元素,按照LCP(lagest contentful paint)的定義,LCP time會返回這張圖片渲染的時間;而咱們的首屏內容亦是這張圖片,那麼咱們的FSP time應該基本等於LCP time,在下面截圖中,也基本驗證了這一點! image.png

最後,對於上面提到的幾個問題,各位讀者有任何見解也可在評論區留言~

倉庫地址:first-screen-paint,歡迎提issue~

參考:

相關文章
相關標籤/搜索