前端不哭!webview 性能優(yōu)化,應(yīng)該關(guān)注哪些指標(biāo)?(好文收藏)
前一段時(shí)間,ELab團(tuán)隊(duì)對(duì) webview 做了一期性能優(yōu)化,在優(yōu)化過(guò)程中,我們追求的是頁(yè)面要足夠「快」「穩(wěn)」。那怎么定量地衡量這個(gè)「快」「穩(wěn)」呢?性能指標(biāo)幫助我們從數(shù)據(jù)化角度了解頁(yè)面性能現(xiàn)狀,性能瓶頸以及優(yōu)化完成后,衡量?jī)?yōu)化效果。
性能指標(biāo)在日常開(kāi)發(fā)中,大家或多或少的都有接觸。比如?? 是一個(gè)頁(yè)面在 Lighthouse 下跑出來(lái)的性能數(shù)據(jù)。

這篇文章將回答下面幾個(gè)問(wèn)題:
哪些性能指標(biāo)是需要觀測(cè)的?它們是什么含義? 這么多指標(biāo),我們?cè)谑裁磮?chǎng)景下應(yīng)該關(guān)注哪些? 指標(biāo)是怎么采集的?
常用的性能指標(biāo)
First Paint First Contentful Paint Largest Contentful Paint First Meaningful Paint First Input Delay Cumulative Layout Shift Time to Interactive DOMContentLoaded Load
First Paint(FP) 首次繪制
首次渲染的時(shí)間點(diǎn),可以視為白屏?xí)r間,比如完成背景色渲染的時(shí)間點(diǎn)。通常作為時(shí)間點(diǎn)最早的一個(gè)性能指標(biāo)。
First Contentful Paint(FCP) 首次內(nèi)容繪制
首次有內(nèi)容渲染的時(shí)間點(diǎn),指標(biāo)測(cè)量頁(yè)面從開(kāi)始加載到頁(yè)面內(nèi)容的任何部分在屏幕上完成渲染的時(shí)間。對(duì)于該指標(biāo),"內(nèi)容"指的是文本、圖像、元素或非白色的元素。可以作為首屏?xí)r間。
FP vs FCP
FP:從開(kāi)始加載到第一次渲染
FCP:從開(kāi)始加載到第一次內(nèi)容渲染。
FCP 是 FP 的增強(qiáng)版,對(duì)用戶來(lái)說(shuō)更關(guān)鍵。因?yàn)?FCP 帶著圖像和文字這些內(nèi)容信息,是用戶更關(guān)心的。
FP 和 FCP 可能是重合的。

Largest Contentful Paint (LCP) 最大內(nèi)容繪制
頁(yè)面的最大內(nèi)容(通常是比較核心的內(nèi)容)加載完成的時(shí)間,這個(gè)最大內(nèi)容可以是圖片/文本塊。它是一個(gè) SEO 相關(guān)的指標(biāo)。
LCP vs FCP
FCP:頁(yè)面加載過(guò)程中,比較早期的一個(gè)指標(biāo),如果一個(gè)頁(yè)面有 loading 態(tài),這個(gè)指標(biāo)表現(xiàn)可能很好,但是實(shí)際內(nèi)容什么時(shí)候呈現(xiàn)給用戶,這個(gè)指標(biāo)沒(méi)辦法衡量。
LCP:關(guān)注頁(yè)面核心內(nèi)容呈現(xiàn)時(shí)間,這個(gè)內(nèi)容是用戶更感興趣的,更加用戶相關(guān)。

First Meaningful Paint(FMP) 首次有效繪制
首次繪制有意義內(nèi)容的時(shí)間。業(yè)界比較認(rèn)可的方式是在加載和渲染過(guò)程中最大布局變動(dòng)之后的那個(gè)繪制時(shí)間即為當(dāng)前頁(yè)面的 FMP。因?yàn)樗?jì)算相對(duì)復(fù)雜,且存在準(zhǔn)確性等問(wèn)題,Lighthouse 6.0 中被廢棄。
LCP vs FMP
FMP: 早期比較推薦的性能指標(biāo),但是計(jì)算更復(fù)雜,而且準(zhǔn)確性不是很好
LCP: 更新的數(shù)據(jù)指標(biāo),有 API 直接支持,統(tǒng)計(jì)簡(jiǎn)單,且準(zhǔn)確,但也存在最大內(nèi)容是否為最核心內(nèi)容這樣的問(wèn)題。
First Input Delay(FID) 首次輸入延遲
這個(gè)指標(biāo)的觸發(fā)是在用戶第一次與頁(yè)面交互的的時(shí)候,記錄的是是用戶第一次與頁(yè)面交互到瀏覽器真正能夠開(kāi)始處理事件處理程序以響應(yīng)該交互的時(shí)間,即交互延遲時(shí)間。比如發(fā)生在用戶第一次在頁(yè)面進(jìn)行 click, keydown 等交互。
為什么會(huì)有這樣的延遲呢?一般來(lái)說(shuō),發(fā)生輸入延遲是因?yàn)闉g覽器的主線程正忙著執(zhí)行其他工作(比如解析和執(zhí)行大型 JS 文件),還不能響應(yīng)用戶。
Cumulative Layout Shift(CLS) 累積布局偏移
在一個(gè)頁(yè)面的生命周期中,會(huì)不斷的發(fā)生布局變化(layout shift),對(duì)每一次布局變化做一個(gè)累積的記分,其中得分最大布局變化即為 CLS。是衡量頁(yè)面穩(wěn)定性的重要指標(biāo)(visual stability)
糟糕的 CLS 對(duì)用戶體驗(yàn)的影響??

Core Web Vitals
2020 年 5 月,Google 提出的衡量網(wǎng)站用戶體驗(yàn)的核心數(shù)據(jù)指標(biāo),涵蓋了頁(yè)面的加載速度、可交互性和穩(wěn)定性。是近期生效的會(huì)影響 SEO 的重要指標(biāo),包含一下三項(xiàng):
LCP FID CLS
Time to Interactive(TTI) 可交互時(shí)間
說(shuō)到 TTI 首先要介紹下 Long Task
Long Task:如果瀏覽器主線程執(zhí)行的一個(gè) task 耗時(shí)大于 50ms,那么這個(gè) task 稱為 long task。用戶的交互操作也是在主線程執(zhí)行的,所以當(dāng)發(fā)生 Long Task 時(shí),用戶的交互操作很可能無(wú)法及時(shí)執(zhí)行,這時(shí)用戶就會(huì)體驗(yàn)到卡頓(當(dāng)頁(yè)面響應(yīng)時(shí)間超過(guò) 100ms 時(shí),用戶可以體驗(yàn)到卡頓),進(jìn)而影響用戶體驗(yàn)。
從頁(yè)面加載開(kāi)始到頁(yè)面處于完全可交互狀態(tài)所花費(fèi)的時(shí)間。通常是發(fā)生在頁(yè)面依賴的資源已經(jīng)加載完成,此時(shí)瀏覽器可以快速響應(yīng)用戶交互的時(shí)間。
DOMContentLoaded(DCL)
DOM 加載完成即觸發(fā),不用等頁(yè)面資源加載。
Load(L)
頁(yè)面及其依賴的資源全部加載完成的時(shí)間,包括所有的資源文件,如樣式表和圖片等。
常見(jiàn)的性能名詞
以用戶為中心的性能指標(biāo) (user-centric metric)
傳統(tǒng)性能指標(biāo):
很長(zhǎng)時(shí)間以來(lái),描述性能是通過(guò) Load/DOMContentLoaded 事件進(jìn)行測(cè)量的,一個(gè)網(wǎng)站加載完成的時(shí)間是多少秒。雖然 Load/DOMContentLoaded 是頁(yè)面生命周期中比較明確的時(shí)刻,但是它真的能很好的反應(yīng)實(shí)際用戶訪問(wèn)頁(yè)面的真實(shí)感受嗎?
比如,服務(wù)器如果響應(yīng)一個(gè)很小的體積的頁(yè)面,Load/DOMContentLoaded 會(huì)很快觸發(fā),然后異步獲取內(nèi)容,之后在頁(yè)面上顯示。這樣的頁(yè)面 Load/DOMContentLoaded 的時(shí)間很短,那從用戶體驗(yàn)角度講它的性能表現(xiàn)就是好的嗎?
要回答上面問(wèn)題,也就引出了?? 這個(gè)概念。
以用戶為中心的性能指標(biāo) (user-centric metric):
以用戶為中心的性能指標(biāo)更關(guān)注從用戶角度看,頁(yè)面的性能是怎樣的?頁(yè)面呈現(xiàn)的內(nèi)容是不是滿足用戶需要,用戶交互起來(lái)是否流暢等。上面介紹的 FCP、LCP、FMP、FID、CLS、TTI 均是以用戶為中心的性能指標(biāo)
| 是否正在發(fā)生? | 導(dǎo)航是否成功啟動(dòng)?服務(wù)器有響應(yīng)嗎? |
|---|---|
| 是否有用? | 是否渲染了足夠的內(nèi)容讓用戶可以深入其中? |
| 是否可用? | 頁(yè)面是否繁忙,用戶是否可以與頁(yè)面進(jìn)行交互? |
| 是否令人愉快? | 交互是否流暢自然,沒(méi)有延遲和卡頓? |
根據(jù)采集方式不同,性能指標(biāo)的幾種形式
根據(jù)測(cè)量方式的不同,性能數(shù)據(jù)可以分為:Lab Data 和 Field Data
Lab Data / SYN
SYN 即 synthetic monitoring,收集形式也有叫 in the lab
Lab Data 是在可控的條件下,特定的機(jī)型,特定的網(wǎng)絡(luò)環(huán)境,收集的性能數(shù)據(jù)。一個(gè)使用場(chǎng)景是,新頁(yè)面開(kāi)發(fā)的時(shí)候,頁(yè)面發(fā)布到生產(chǎn)環(huán)境中之前,是沒(méi)辦法基于真實(shí)用戶做性能指標(biāo)測(cè)量的,此時(shí),想了解也沒(méi)性能情況,可以通過(guò) Lab 方式收集和檢查。
Field Data / RUM
RUM 即 Real User Monitoring,收集形式也有叫 in the Filed
Lab 的方式測(cè)量雖然能反應(yīng)性能情況,但是真實(shí)用戶因機(jī)型和網(wǎng)絡(luò)情況各異,頁(yè)面加載對(duì)于不同用戶具有很大的不確定性,Lab 數(shù)據(jù)并不一定是真實(shí)用戶的實(shí)際情況。而 filed 數(shù)據(jù)很好的解決了這個(gè)問(wèn)題,有一定的代碼侵入性,記錄真實(shí)用戶的性能數(shù)據(jù),通過(guò) RUM 數(shù)據(jù)可以發(fā)現(xiàn)一些 Lab 數(shù)據(jù)下很難暴露出來(lái)的性能異常。
RAIL Model
我們先來(lái)看下 RAIL 這幾個(gè)字母分別對(duì)應(yīng)什么?
R: response
A: animation
I: idle
L: load
RAIL 是 Google 提出的以用戶為中心的一套性能模型,從各個(gè)維度反應(yīng)一個(gè)網(wǎng)站的性能情況,同時(shí)提供一組性能目標(biāo)供參考??
Response: 50ms 內(nèi)對(duì)事件做出響應(yīng) Animation: 動(dòng)畫 10 ms 內(nèi)生成內(nèi)一幀(每幀耗時(shí) 16ms,用戶會(huì)感到動(dòng)畫流暢,為什么這里是 10ms?) Idle: 最大化的利用上空閑時(shí)間 Load: 在 5s 內(nèi)完成內(nèi)容傳輸并達(dá)到用戶可交互
性能指標(biāo)是怎么采集的
性能指標(biāo)相關(guān)的 API 及采集方式
Performance Observer[1]
Performance Observer API 下包含一組性能監(jiān)測(cè)相關(guān)的 API
Paint Timing API
Largest Contentful Paint API
Event Timing API
Navigation Timing API
Layout Instability API
Long Tasks API
Resource Timing API
下面按照不同指標(biāo)的收集用到的 API 依次介紹它們是怎么用的。
Paint Timing API[2]
用于收集 FP / FCP
new?PerformanceObserver((list)?=>?{
??for?(const?entry?of?list.getEntries())?{
????console.log('FCP:?',?entry.startTime);
??}
}).observe({
??type:?'first-contentful-paint',
??buffered:?true,
});
new?PerformanceObserver((list)?=>?{
??for?(const?entry?of?list.getEntries())?{
????console.log('FP:?',?entry.startTime);
??}
}).observe({
??type:?'first-paint',
??buffered:?true,
});
Largest Contentful Paint API[3]
用于收集 LCP
largest-contentful-paint 事件會(huì)在頁(yè)面加載過(guò)程中根據(jù)此時(shí)已渲染最大元素的變化,不斷的被觸發(fā),實(shí)際上報(bào)中會(huì)一直監(jiān)聽(tīng)這些變化,直到用戶與頁(yè)面發(fā)生交互行為(比如 click、keydown)或者頁(yè)面被隱藏或者頁(yè)面被 unload 等,取監(jiān)聽(tīng)到的最后值做上報(bào)。
new?PerformanceObserver(entryList?=>?{
??for?(const?entry?of?entryList.getEntries())?{
????console.log('LCP:?',?entry.startTime);
??}
}).observe({
????type:?'largest-contentful-paint',
????buffered:?true
});

完整版代碼參考???
https://github.com/GoogleChrome/web-vitals/blob/main/src/getLCP.ts
這個(gè)指標(biāo)是 Core Web Vitals,但是兼容性不好,iOS 下都是不支持的??

Event Timing API[4]
用于收集 FID
監(jiān)聽(tīng)用戶的第一次輸入(first-input)事件,F(xiàn)ID = 開(kāi)始處理 input 的時(shí)間 - input 操作的起始時(shí)間
new?PerformanceObserver(list?=>?{
??for?(const?entry?of?list.getEntries())?{
????//?開(kāi)始處理?input?的時(shí)間?-?input?操作的起始時(shí)間
????const?FID?=?entry.processingStart?-?entry.startTime;?
????console.log('FID:',?FID);
??}
}).observe({
??type:?'first-input',
??buffered:?true,
});
Navigation Timing 1.0[5] 或 Navigation Timing 2.0[6]
用于收集 Load / DOMContentLoaded

new?PerformanceObserver(list?=>?{
??for?(const?entry?of?list.getEntries())?{
????const?Load?=?entry.loadEventStart?-?entry.fetchStart;
????console.log('Load:',?Load);
??}
}).observe({
??type:?'navigation',
??buffered:?true,
});
new?PerformanceObserver(list?=>?{
??for?(const?entry?of?list.getEntries())?{
????const?DOMContentLoaded?=?entry.domContentLoadedEventStart?-?entry.fetchStart;
????console.log('DOMContentLoaded:',?DOMContentLoaded);
??}
}).observe({
??type:?'navigation',
??buffered:?true,
});
Layout Instability API[7]
收集 CLS: 將加載過(guò)程分塊(session),監(jiān)聽(tīng) layout-shift 變化獲得每次布局變動(dòng)的 value 值,統(tǒng)計(jì)每個(gè) session 的布局變動(dòng)分?jǐn)?shù),最大的布局變動(dòng)分?jǐn)?shù)時(shí)段,即為 CLS。
let?sessionEntries?=?[];
let?sessionValue?=?0;
let?metric?=?{
??value:?0
}
new?PerformanceObserver(entryList?=>?{
??for?(const?entry?of?entryList.getEntries())?{
????if?(!entry.hadRecentInput)?{
??????const?firstSessionEntry?=?sessionEntries[0];
??????const?lastSessionEntry?=?sessionEntries[sessionEntries.length?-?1];
??????//?如果時(shí)間靠近上一個(gè)?session,?將本輪?layout-shift?累加進(jìn)上一個(gè)?session
??????if?(sessionValue?&&
??????????entry.startTime?-?lastSessionEntry.startTime?1000?&&
??????????entry.startTime?-?firstSessionEntry.startTime?5000)?{
????????sessionValue?+=?entry.value;
????????sessionEntries.push(entry);
??????}?else?{?//?新起一個(gè)?session
????????sessionValue?=?entry.value;
????????sessionEntries?=?[entry];
??????}
??????//?如果當(dāng)前?session?的?value?大于之前的最大值,替換為現(xiàn)在這個(gè)大的
??????if?(sessionValue?>?metric.value)?{
????????metric.value?=?sessionValue;
????????metric.entries?=?sessionEntries;
????????console.log('CLS:?',?metric)
??????}
????}
??}
}).observe({
????type:?'layout-shift',
????buffered:?true
});
完整代碼見(jiàn)???
https://github.com/GoogleChrome/web-vitals/blob/main/src/getCLS.ts
Long Tasks API[8] & Resource Timing API[9]
TTI 的采集依賴這兩個(gè) API,計(jì)算過(guò)程:
先采集 FCP,作為起點(diǎn) 沿時(shí)間軸正向搜索時(shí)長(zhǎng)至少為 5 秒的安靜窗口(安靜窗口:沒(méi)有 Long Task 且不超過(guò)兩個(gè)正在處理的網(wǎng)絡(luò) GET 請(qǐng)求) 沿時(shí)間軸反向搜索安靜窗口之前的最后一個(gè)長(zhǎng)任務(wù),如果沒(méi)有找到長(zhǎng)任務(wù),則在 FCP 步驟停止執(zhí)行 TTI 是安靜窗口之前最后一個(gè)長(zhǎng)任務(wù)的結(jié)束時(shí)間,如果沒(méi)有找到長(zhǎng)任務(wù),則與 FCP 值相同 —— Time to Interactive (TTI)[10]

Mutation[11] [12]Observer[13]
收集 FMP:通過(guò) MutationObserver 對(duì) DOM 變化進(jìn)行監(jiān)聽(tīng),每次回調(diào)根據(jù)新舊 DOM 數(shù)量、種類、深度等,計(jì)算出當(dāng)前 DOM 樹的分?jǐn)?shù),分?jǐn)?shù)變化最劇烈的時(shí)刻視為 FMP ,Load 事件觸發(fā)后 200ms 停止監(jiān)聽(tīng),取最大變動(dòng)的記錄做上報(bào)。
new?MutationObserver(()?=>?{
??//?這里是劇烈程度分的計(jì)算
}).observe(document,?{
??childList:?true,
??subtree:?true,
});
業(yè)界性能指標(biāo)采集工具
In the Lab
Chrome DevTools[14]
Lighthouse[15]
WebPageTest[16]
In the Field
web-vitals[17]
參考
Web Vitals[18]
Largest Contentful Paint (LCP)[19]
Cumulative Layout Shift (CLS)[20]
First Input Delay (FID)[21]
Measure performance with the RAIL model[22]
?CocosStore 雙11來(lái)臨!限時(shí)5折優(yōu)惠開(kāi)啟
參與活動(dòng)領(lǐng)取Cocos定制周邊
明天 11.11 Cocos微店將全面開(kāi)啟限時(shí)5折優(yōu)惠
CocosStore 部分作品參與活動(dòng)
游戲開(kāi)發(fā),技術(shù)變現(xiàn),期待你的參與!
