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