前言
大家好,我是林三心,用最通俗易懂的話講最難的知識點是我的座右銘,基礎是進階的前提是我的初心
背景
當我們在做專案的效能優化的時候,優化首屏時間是一個避不過去的優化方向,但是又有多少人想過這兩個東西的區別呢:
白屏時間
首屏時間
並且這兩個時間的計算方式又有什麼區別呢?接下來我就給大家講一下吧!
白屏時間
是什麼?
白屏時間指的是:頁面開始顯示內容的時間。也就是:瀏覽器顯示第一個字元或者元素的時間
怎麼算?
我們只需要知道瀏覽器開始顯示內容的時間點,即頁面白屏結束時間點即可獲取到頁面的白屏時間。
因此,我們通常認為瀏覽器開始渲染 <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
總結
我這個計算方法其實很多漏洞,沒把刪除元素也考慮進去,但是想讓大家知道計算首屏時間的計算思想,這才是最重要的,希望大家能理解這個計算思想
結語
我是林三心,一個熱心的前端菜鳥程式設計師。如果你上進,喜歡前端,想學習前端,那我們們可以交朋友,一起摸魚哈哈,摸魚群,加我請備註【思否】