首屏時間,你說你優化了,那你倒是計算出給給我看啊!

Sunshine_Lin發表於2022-02-18

前言

大家好,我是林三心,用最通俗易懂的話講最難的知識點是我的座右銘,基礎是進階的前提是我的初心

背景

當我們在做專案的效能優化的時候,優化首屏時間是一個避不過去的優化方向,但是又有多少人想過這兩個東西的區別呢:

  • 白屏時間
  • 首屏時間

並且這兩個時間的計算方式又有什麼區別呢?接下來我就給大家講一下吧!

白屏時間

是什麼?

白屏時間指的是:頁面開始顯示內容的時間。也就是:瀏覽器顯示第一個字元或者元素的時間

怎麼算?

我們只需要知道瀏覽器開始顯示內容的時間點,即頁面白屏結束時間點即可獲取到頁面的白屏時間。

因此,我們通常認為瀏覽器開始渲染 <body> 標籤或者解析完 <head> 標籤的時刻就是頁面白屏結束的時間點。

  • 瀏覽器支援 performance.timing

    <head>
    <title>Document</title>
    </head>
    <script type="text/javascript">
    // 白屏時間結束點
    var firstPaint = Date.now()
    var start = performance.timing.navigationStart
    console.log(firstPaint - start)
    </script>
  • 瀏覽器不支援 performance.timing

    <head>
    <title>Document</title>
    <script type="text/javascript">
      window.start = Date.now();
    </script>
    </head>
    <script type="text/javascript">
    // 白屏時間結束點
    var firstPaint = Date.now()
    console.log(firstPaint - window.start)
    </script>

    首屏時間

    是什麼?

    首屏時間是指使用者開啟網站開始,到瀏覽器首屏內容渲染完成的時間。對於使用者體驗來說,首屏時間是使用者對一個網站的重要體驗因素

為什麼不直接用生命週期?

有些小夥伴會說:為啥不直接在App.vue的 mounted 生命週期裡計算時間呢?大家可以看看,官網說了 mounted 執行並不代表首屏所有元素載入完畢,所以 mounted 計算出來的時間會偏短。

為什麼不直接用nextTick?

nextTick 回撥的時候,首屏的DOM都渲染出來了,但是計算 首屏時間 並不需要渲染所有DOM,所以計算出來的時間會偏長

怎麼算?

我們需要利用 MutationObserver 監控DOM的變化,監控每一次DOM變化的分數,計算的規則為:
(1 + 層數 * 0.5),我舉個例子:

<body>
    <div>
      <div>1</div>
      <div>2</div>
    </div>
</body>

以上DOM結構的分數為:

1.5 + 2 + 2.5 + 2.5 = 8.5(分)

其實在首屏的載入中,會涉及到DOM的增加、修改、刪除,所以會觸發多次 MutationObserver ,所以會統計出不同階段的score,我們把這些score存放在一個陣列 observerData 中,後面大有用處

首屏時間實踐

現在我們開始計算首屏時間吧!

前置準備

  • index.html:html頁面

    <!DOCTYPE html>
    <html lang="en">
    <head> </head>
    <body>
      <div>
        <div>
          <div>1</div>
          <div>2</div>
        </div>
        <div>3</div>
        <div>4</div>
      </div>
      <ul id="ulbox"></ul>
    </body>
    <script src="./computed.js"></script>
    <script src="./request.js"></script>
    </html>
  • computed.js :計算首屏時間的檔案

    const observerData = []
    
    let observer = new MutationObserver(() => {
    // 計算每次DOM修改時,距離頁面剛開始載入的時間
    const start = window.performance.timing.navigationStart
    const time = new Date().getTime() - start
    
    const body = document.querySelector('body')
    const score = computedScore(body, 1)
    // 加到陣列 observerData 中
    observerData.push({
      score,
      time
    })
    })
    observer.observe(
    document, {
      childList: true,
      subtree: true
    }
    )
    
    function computedScore(element, layer) {
    let score = 0
    const tagName = element.tagName
    // 排除這些標籤的情況
    if (
      tagName !== 'SCRIPT' &&
      tagName !== 'STYLE' &&
      tagName !== 'META' &&
      tagName !== 'HEAD'
    ) {
      const children = element.children
      if (children && children.length) {
        // 遞迴計算分數
        for (let i = 0; i < children.length; i++) {
          score += computedScore(children[i], layer + 1)
        }
      }
    
      score += 1 + 0.5 * layer
    }
    return score
    }
  • request.js :模擬請求修改DOM

    // 模擬請求列表
    const requestList = () => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(
          [1, 2, 3,
            4, 5, 6,
            7, 8, 9
          ]
        )
      }, 1000)
    })
    }
    
    const ulbox = document.getElementById('ulbox')
    
    // 模擬請求資料渲染列表
    const renderList = async () => {
    const list = await requestList()
    const fragment = document.createDocumentFragment()
    for (let i = 0; i < list.length; i++) {
      const li = document.createElement('li')
      li.innerText = list[i]
      fragment.appendChild(li)
    }
    ulbox.appendChild(fragment)
    }
    
    // 模擬對列表進行輕微修改
    const addList = async () => {
    const li = document.createElement('li')
    li.innerText = '加上去'
    ulbox.appendChild(li)
    }
    
    (async () => {
    // 模擬請求資料渲染列表
    await renderList()
    // 模擬對列表進行輕微修改
    addList()
    })()

observerData

當我們一切準備就緒後執行程式碼,我們獲得了 observerData ,我們看看它長什麼樣?

計算首屏時間

我們怎麼根據 observerData 來計算首屏時間呢?我們可以這麼算:下次分數比上次分數增加幅度最大的時間作為首屏時間

很多人會問了,為什麼不是取最後一項的時間來當做首屏時間呢?大家要注意了:首屏並不是所有DOM都渲染,我就拿剛剛的程式碼來舉例吧,我們渲染完了列表,然後再去增加一個li,那你是覺得哪個時間段算是首屏呢?應該是渲染完列表後算首屏完成,因為後面只增加了一個li,分數的漲幅較小,可以忽略不計

所以我們開始計算吧:

const observerData = []

let observer = new MutationObserver(() => {
  // 計算每次DOM修改時,距離頁面剛開始載入的時間
  const start = window.performance.timing.navigationStart
  const time = new Date().getTime() - start
  const body = document.querySelector('body')
  const score = computedScore(body, 1)
  observerData.push({
    score,
    time
  })

  // complete時去呼叫 unmountObserver
  if (document.readyState === 'complete') {
    // 只計算10秒內渲染時間
    unmountObserver(10000)
  }
})
observer.observe(
  document, {
    childList: true,
    subtree: true
  }
)

function computedScore(element, layer) {
  let score = 0
  const tagName = element.tagName
  // 排除這些標籤的情況
  if (
    tagName !== 'SCRIPT' &&
    tagName !== 'STYLE' &&
    tagName !== 'META' &&
    tagName !== 'HEAD'
  ) {
    const children = element.children
    if (children && children.length) {
      // 遞迴計算分數
      for (let i = 0; i < children.length; i++) {
        score += computedScore(children[i], layer + 1)
      }
    }

    score += 1 + 0.5 * layer
  }
  return score
}

// 計算首屏時間
function getFirstScreenTime() {
  let data = null
  for (let i = 1; i < observerData.length; i++) {
    // 計算幅度
    const differ = observerData[i].score - observerData[i - 1].score
    // 取最大幅度,記錄對應時間
    if (!data || data.rate <= differ) {
      data = {
        time: observerData[i].time,
        rate: differ
      }
    }
  }
  return data
}

let timer = null

function unmountObserver(delay) {
  if (timer) return
  timer = setTimeout(() => {
    // 輸出首屏時間
    console.log(getFirstScreenTime())
    // 終止MutationObserver的監控
    observer.disconnect()
    observer = null
    clearTimeout(timer)
  }, delay)
}

計算出首屏時間 1020ms

總結

我這個計算方法其實很多漏洞,沒把刪除元素也考慮進去,但是想讓大家知道計算首屏時間的計算思想,這才是最重要的,希望大家能理解這個計算思想

結語

我是林三心,一個熱心的前端菜鳥程式設計師。如果你上進,喜歡前端,想學習前端,那我們們可以交朋友,一起摸魚哈哈,摸魚群,加我請備註【思否】

image.png

相關文章