首屏?xí)r間,你說你優(yōu)化了,那你倒是計算出來給我看??!
前言
大家好,我是林三心,用最通俗易懂的話講最難的知識點是我的座右銘,基礎(chǔ)是進(jìn)階的前提是我的初心
背景
當(dāng)我們在做項目的性能優(yōu)化的時候,優(yōu)化首屏?xí)r間是一個避不過去的優(yōu)化方向,但是又有多少人想過這兩個東西的區(qū)別呢:
- 白屏?xí)r間
- 首屏?xí)r間
并且這兩個時間的計算方式又有什么區(qū)別呢?接下來我就給大家講一下吧!
白屏?xí)r間
是什么?
白屏?xí)r間指的是:頁面開始顯示內(nèi)容的時間。也就是:瀏覽器顯示第一個字符或者元素的時間
圖片
怎么算?
我們只需要知道瀏覽器開始顯示內(nèi)容的時間點,即頁面白屏結(jié)束時間點即可獲取到頁面的白屏?xí)r間。
因此,我們通常認(rèn)為瀏覽器開始渲染<body>標(biāo)簽或者解析完<head>標(biāo)簽的時刻就是頁面白屏結(jié)束的時間點。
- 瀏覽器支持performance.timing
<head>
<title>Document</title>
</head>
<script type="text/javascript">
// 白屏?xí)r間結(jié)束點
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">
// 白屏?xí)r間結(jié)束點
var firstPaint = Date.now()
console.log(firstPaint - window.start)
</script>
首屏?xí)r間
是什么?
首屏?xí)r間是指用戶打開網(wǎng)站開始,到瀏覽器首屏內(nèi)容渲染完成的時間。對于用戶體驗來說,首屏?xí)r間是用戶對一個網(wǎng)站的重要體驗因素。
圖片
為什么不直接用生命周期?
有些小伙伴會說:為啥不直接在App.vue的mounted生命周期里計算時間呢?大家可以看看,官網(wǎng)說了mounted執(zhí)行并不代表首屏所有元素加載完畢,所以mounted計算出來的時間會偏短。
圖片
為什么不直接用nextTick?
nextTick回調(diào)的時候,首屏的DOM都渲染出來了,但是計算首屏?xí)r間并不需要渲染所有DOM,所以計算出來的時間會偏長
怎么算?
我們需要利用MutationObserver監(jiān)控DOM的變化,監(jiān)控每一次DOM變化的分?jǐn)?shù),計算的規(guī)則為: (1 + 層數(shù) * 0.5),我舉個例子:
<body>
<div>
<div>1</div>
<div>2</div>
</div>
</body>
以上DOM結(jié)構(gòu)的分?jǐn)?shù)為:
1.5 + 2 + 2.5 + 2.5 = 8.5(分)
圖片
其實在首屏的加載中,會涉及到DOM的增加、修改、刪除,所以會觸發(fā)多次MutationObserver,所以會統(tǒng)計出不同階段的score,我們把這些score存放在一個數(shù)組observerData中,后面大有用處
首屏?xí)r間實踐
現(xiàn)在我們開始計算首屏?xí)r間吧!
前置準(zhǔn)備
- 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:計算首屏?xí)r間的文件
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)
// 加到數(shù)組 observerData 中
observerData.push({
score,
time
})
})
observer.observe(
document, {
childList: true,
subtree: true
}
)
function computedScore(element, layer) {
let score = 0
const tagName = element.tagName
// 排除這些標(biāo)簽的情況
if (
tagName !== 'SCRIPT' &&
tagName !== 'STYLE' &&
tagName !== 'META' &&
tagName !== 'HEAD'
) {
const children = element.children
if (children && children.length) {
// 遞歸計算分?jǐn)?shù)
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')
// 模擬請求數(shù)據(jù)渲染列表
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)
}
// 模擬對列表進(jìn)行輕微修改
const addList = async () => {
const li = document.createElement('li')
li.innerText = '加上去'
ulbox.appendChild(li)
}
(async () => {
// 模擬請求數(shù)據(jù)渲染列表
await renderList()
// 模擬對列表進(jìn)行輕微修改
addList()
})()
observerData
當(dāng)我們一切準(zhǔn)備就緒后運行代碼,我們獲得了observerData,我們看看它長什么樣?
計算首屏?xí)r間
我們怎么根據(jù)observerData來計算首屏?xí)r間呢?我們可以這么算:下次分?jǐn)?shù)比上次分?jǐn)?shù)增加幅度最大的時間作為首屏?xí)r間
很多人會問了,為什么不是取最后一項的時間來當(dāng)做首屏?xí)r間呢?大家要注意了:首屏并不是所有DOM都渲染,我就拿剛剛的代碼來舉例吧,我們渲染完了列表,然后再去增加一個li,那你是覺得哪個時間段算是首屏呢?應(yīng)該是渲染完列表后算首屏完成,因為后面只增加了一個li,分?jǐn)?shù)的漲幅較小,可以忽略不計
所以我們開始計算吧:
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時去調(diào)用 unmountObserver
if (document.readyState === 'complete') {
// 只計算10秒內(nèi)渲染時間
unmountObserver(10000)
}
})
observer.observe(
document, {
childList: true,
subtree: true
}
)
function computedScore(element, layer) {
let score = 0
const tagName = element.tagName
// 排除這些標(biāo)簽的情況
if (
tagName !== 'SCRIPT' &&
tagName !== 'STYLE' &&
tagName !== 'META' &&
tagName !== 'HEAD'
) {
const children = element.children
if (children && children.length) {
// 遞歸計算分?jǐn)?shù)
for (let i = 0; i < children.length; i++) {
score += computedScore(children[i], layer + 1)
}
}
score += 1 + 0.5 * layer
}
return score
}
// 計算首屏?xí)r間
function getFirstScreenTime() {
let data = null
for (let i = 1; i < observerData.length; i++) {
// 計算幅度
const differ = observerData[i].score - observerData[i - 1].score
// 取最大幅度,記錄對應(yīng)時間
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(() => {
// 輸出首屏?xí)r間
console.log(getFirstScreenTime())
// 終止MutationObserver的監(jiān)控
observer.disconnect()
observer = null
clearTimeout(timer)
}, delay)
}
計算出首屏?xí)r間1020ms
總結(jié)
我這個計算方法其實很多漏洞,沒把刪除元素也考慮進(jìn)去,但是想讓大家知道計算首屏?xí)r間的計算思想,這才是最重要的,希望大家能理解這個計算思想。