You can't manage what you can't measure. —— Peter Drucker。
度量
引語中提到了 彼得·德魯克 的一句話,“一件事如果你無法衡量它、你就無法管理它”,性能同樣如此。如果沒有一個(gè)準(zhǔn)確的方案來對(duì)性能進(jìn)行度量,那優(yōu)化就無從談起。
那么對(duì)于我們來說,哪些指標(biāo)是可以用來對(duì)頁(yè)面性能、用戶體驗(yàn)進(jìn)行度量的呢?我會(huì)從如下幾個(gè)角度逐一為大家講解:
- Performance API
- 頁(yè)面流暢度
FPS
raf(requestAnimationFrame)
- 首屏性能
FP、FCP、FMP
CWV(Core Web Vitals)
Performance API
相信前端的同學(xué)對(duì)于 Performance API 應(yīng)該都不陌生,通常我們將瀏覽器提供的可以進(jìn)行測(cè)算和采集的 API 統(tǒng)稱為 Performance API,該類型的對(duì)象可以通過調(diào)用只讀屬性 window.performance 來獲得。
在日常的工作中,我們?yōu)榱擞?jì)算一個(gè)任務(wù)的耗時(shí),通常會(huì)在任務(wù)執(zhí)行前后利用 Date.now() 分別創(chuàng)建兩個(gè)時(shí)間,最后取兩者的差值作為任務(wù)執(zhí)行耗時(shí)。但是 Date.now() 存在兩個(gè)問題:
- 返回的時(shí)間戳是從 1970 年 1 月 1 日 00:00:00 UTC 開始經(jīng)過的毫秒數(shù),依賴于系統(tǒng)時(shí)間。
- 精度僅到毫秒(ms)。
為了對(duì)性能做精準(zhǔn)的計(jì)算,我們可以選擇 Performance API 提供的 performance.now() 來進(jìn)行高精度計(jì)算,其特性為:
- 時(shí)間戳基于頁(yè)面打開的時(shí)間計(jì)算。
- 精度精確到微秒(us)。
下圖可以比較直觀的看到兩者的差異:?
頁(yè)面流暢度
FPS
幀率 FPS(Frames Per Second - 每秒傳輸幀數(shù)),一般對(duì)于網(wǎng)頁(yè)而言,最優(yōu)的幀率在 60 FPS,如果越接近這個(gè)值,頁(yè)面就越流暢,幀率如果遠(yuǎn)低于這個(gè)值,用戶可能會(huì)明顯感覺到卡頓。
60 FPS 意味著頁(yè)面每隔 16.5ms(1/60)就需要渲染一次,否則就會(huì)出現(xiàn)丟幀的現(xiàn)象,而瀏覽器中的 JavaScript 執(zhí)行和頁(yè)面渲染都是會(huì)相互阻塞的,如果在代碼中有非常復(fù)雜的邏輯占用了大量的執(zhí)行時(shí)長(zhǎng),就會(huì)導(dǎo)致頁(yè)面出現(xiàn)卡頓。
在 Chrome 的 devtools 中我們可以執(zhí)行 Cmd+Shift+P 輸入 show fps 來快速打開 fps 面板,如下圖所示:
通過觀察 FPS 面板,我們可以很方便的對(duì)當(dāng)前頁(yè)面的流暢度進(jìn)行監(jiān)控。
我們?cè)诖a中如果想對(duì)當(dāng)前頁(yè)面的 FPS 幀率進(jìn)行監(jiān)控,可以參考如下這段示例代碼:
var lastTime = performance.now();
var frame = 0;
var lastFameTime = performance.now();
var loop = function(time) {
var now = performance.now();
var fs = (now - lastFameTime);
lastFameTime = now;
var fps = Math.round(1000/fs);
frame++;
if(now > 1000 + lastTime) {
var fps = Math.round((frame * 1000) / (now - lastTime));
frame = 0;
lastTime = now;
};
window.requestAnimationFrame(loop);
}
requestAnimationFrame
window.requestAnimationFrame() 告訴瀏覽器你希望執(zhí)行一個(gè)動(dòng)畫,并且要求瀏覽器在下次重繪之前調(diào)用指定的回調(diào)函數(shù)更新動(dòng)畫。
這里借用 MDN 的描述,顧名思義就是傳入一個(gè)函數(shù),讓瀏覽器在下一次渲染之前進(jìn)行調(diào)用。那么基于這個(gè)特性,結(jié)合上面提供的 FPS 計(jì)算示例代碼,我們可以發(fā)現(xiàn),如果我們持續(xù)對(duì) requestAnimationFrame 進(jìn)行調(diào)用,那么每次調(diào)用的間隔應(yīng)該在 16.7ms 左右,即滿足我們對(duì)于頁(yè)面流暢度 60 FPS 的要求,可以使用如下代碼在控制臺(tái)執(zhí)行試試看:
let lastTime = 0;
const measure = () => {
console.log(`${Date.now() - lastTime}ms`);
lastTime = Date.now();
requestAnimationFrame(measure);
};
measure();
首屏性能
首屏性能作為我們最關(guān)心的核心指標(biāo)之一,在性能優(yōu)化的場(chǎng)景中占據(jù)了相當(dāng)大的比重,那么對(duì)于首屏的性能我們有哪些衡量指標(biāo)呢?
針對(duì)這個(gè)問題,Google 曾經(jīng)提出過一系列的以用戶體驗(yàn)為中心的性能指標(biāo)。
FP、FCP、FMP
FP(First Paint 譯為“首次繪制”)代表瀏覽器第一次向屏幕傳輸像素的時(shí)間,僅表示當(dāng)前已經(jīng)開始繪制了,實(shí)際意義比較小。
FCP(First Contentful Paint 譯為“首次內(nèi)容繪制”)代表瀏覽器第一次向屏幕繪制 “內(nèi)容”(只有首次繪制文本、圖片(包含背景圖)、非白色)。
相比之下,F(xiàn)CP 指的是瀏覽器首次繪制來自 DOM 的內(nèi)容。例如:文本,圖片,SVG,canvas元素等,這個(gè)時(shí)間點(diǎn)叫 FCP。
FMP(First Meaningful Paint 譯為“首次有效繪制”)表示頁(yè)面中有意義的內(nèi)容開始出現(xiàn)在屏幕上的時(shí)間點(diǎn)。它也是我們來衡量用戶加載體驗(yàn)的主要指標(biāo)。
FMP 本質(zhì)上是一個(gè)主觀認(rèn)知指標(biāo),是通過一個(gè)算法來猜測(cè)某個(gè)時(shí)間點(diǎn)可能是 FMP,但是計(jì)算方式過于復(fù)雜而且不準(zhǔn)確,后來 Google 也放棄了 FMP 的探測(cè)算法,轉(zhuǎn)而采用更加明確的客觀指標(biāo) - LCP。
CWV(Core Web Vitals)
核心 Web 指標(biāo)是適用于所有網(wǎng)頁(yè)的 Web 指標(biāo)子集,每位網(wǎng)站所有者都應(yīng)該測(cè)量這些指標(biāo),并且這些指標(biāo)還將顯示在所有 Google 工具中。每項(xiàng)核心 Web 指標(biāo)代表用戶體驗(yàn)的一個(gè)不同方面,能夠進(jìn)行實(shí)際測(cè)量,并且反映出以用戶為中心的關(guān)鍵結(jié)果的真實(shí)體驗(yàn)。
目前的 Web 核心指標(biāo)由三個(gè)方面構(gòu)成 — 頁(yè)面加載性能、交互性、視覺穩(wěn)定性,包含如下三個(gè)指標(biāo)及閾值:
- ?Largest Contentful Paint (LCP):最大內(nèi)容繪制,測(cè)量加載性能。為了提供良好的用戶體驗(yàn),LCP 應(yīng)在頁(yè)面首次開始加載后的2.5 秒內(nèi)發(fā)生。
- ?First Input Delay (FID):首次輸入延遲,測(cè)量交互性。為了提供良好的用戶體驗(yàn),頁(yè)面的 FID 應(yīng)為100 毫秒或更短。
- ?Cumulative Layout Shift (CLS):累積布局偏移,測(cè)量視覺穩(wěn)定性。為了提供良好的用戶體驗(yàn),頁(yè)面的 CLS 應(yīng)保持在 0.1. 或更少。
1)LCP
LCP 關(guān)注的是首屏中最大元素渲染渲染的時(shí)間,和 FCP 不同的是,F(xiàn)CP 更關(guān)注瀏覽器什么時(shí)候開始繪制內(nèi)容,比如一個(gè) loading 頁(yè)面或者骨架屏,并沒有實(shí)際價(jià)值,所以 LCP 相較于 FCP 更適合作為首屏指標(biāo)。
拿 Detail 頁(yè)舉例,在 FCP 時(shí),商品圖片并未加載,此時(shí)對(duì)于用戶而言,一個(gè)近乎白屏的頁(yè)面是不具備可交互價(jià)值的,在 LCP 時(shí),圖片已經(jīng)完成了加載,首屏主要元素也幾乎加載完畢,此時(shí)的時(shí)間作為首屏?xí)r間,才是比較接近用戶體感的。
既然 LCP 是根據(jù)頁(yè)面上占據(jù)面積最大的元素渲染時(shí)間確定的,那么元素包含哪些呢?
- 圖片
- 內(nèi)嵌在 svg 中的 image 元素
- 視頻的封面
- 通過 url() 加載的 background image
- 文字
在 webpagetest 上可以很直觀的看到當(dāng)前 LCP 元素的詳情信息:
元素面積的計(jì)算規(guī)則有如下幾點(diǎn):
- 在 viewport 內(nèi)可見元素的大小,如果是超出可視區(qū)域或者被裁減、遮擋等,都不算入該元素大小。
- 對(duì)于圖片元素來說,大小是取圖片實(shí)際大小和原始大小的較小值,即Min(實(shí)際大小,原始大小)。
- 對(duì)于文字元素,只取能夠覆蓋文字的最小矩形面積
- 對(duì)所有元素,margin、padding、border 等都不算。
2)FID
FID 指標(biāo)是指用戶首次和網(wǎng)站進(jìn)行交互到瀏覽器響應(yīng)該事件的實(shí)際延時(shí)時(shí)間,可以想象一下,如果在你點(diǎn)擊了一個(gè) button 后,頁(yè)面沒有任何變化,2-3s 后才開始響應(yīng),可想而知體驗(yàn)是非常糟糕的。
FID 判定的交互行為有:
- 點(diǎn)擊、觸摸、按鍵等(不包含滾動(dòng)和縮放)。
- 有事件綁定的行為,比如注冊(cè)在某個(gè) dom 上的 click 事件。
那么為什么會(huì)產(chǎn)生交互延遲呢?比如我在 button 上注冊(cè)了一個(gè) click 事件,例如:
btn.addEventListener('click', () => {
// do something
})
按照預(yù)期,用戶點(diǎn)擊按鈕的時(shí)候,回調(diào)函數(shù)會(huì)被直接觸發(fā),但是如果當(dāng)前主線程被渲染、Long Tasks 占用,這個(gè)回調(diào)的執(zhí)行就會(huì)被延后,就會(huì)導(dǎo)致 FID 時(shí)長(zhǎng)增加。
但是 FID 作為一個(gè)“非客觀值”,需要用戶進(jìn)行交互才能采集到,用戶的交互時(shí)機(jī),同樣也會(huì)對(duì)指標(biāo)的采集、統(tǒng)計(jì)造成影響。
3)CLS
CLS 是用來衡量視覺界面穩(wěn)定性的一個(gè)指標(biāo),指的是頁(yè)面產(chǎn)生的連續(xù)累計(jì)布局偏移分?jǐn)?shù)。我們?cè)谌粘I(yè)務(wù)中經(jīng)常會(huì)用到懶加載、骨架屏等方式,用較低的成本先展示頁(yè)面框架,再用動(dòng)態(tài)渲染的方式,來對(duì)頁(yè)面內(nèi)容進(jìn)行填充,如果此時(shí)布局發(fā)生變化,比如動(dòng)態(tài)加載的元素和原本占位的元素大小不一致,可能就會(huì)導(dǎo)致用戶誤操作,影響用戶體驗(yàn),CLS 就是為了度量這類問題而存在。
當(dāng)我們?cè)谡f布局偏移的時(shí)候,指的是:頁(yè)面中一個(gè)可見元素的起始位置發(fā)生改變,而元素的增刪則并不會(huì)觸發(fā)布局偏移。
那么如何定義偏移的連續(xù)累計(jì)呢?有如下幾個(gè)要素:
- CLS 計(jì)算的并非頁(yè)面整個(gè)周期的偏移分?jǐn)?shù)之和,而是累計(jì)值最高的連續(xù)布局偏移。
- 偏移相隔的時(shí)間少于 1s,且整個(gè)窗口的最大持續(xù)時(shí)間為 5s,則被計(jì)為連續(xù)偏移。
分析工具
DevTools
DevTools 算是和前端同學(xué)打交道最多的工具之一了,主要用來查看日志、查看網(wǎng)絡(luò)請(qǐng)求、debug 頁(yè)面等等,我們同時(shí)還可以利用它對(duì)頁(yè)面性能進(jìn)行分析。
Network
如上圖所示,這是我們很熟悉的 Network 界面,功能上我用紅框大概做了一下劃分:
- 選項(xiàng)區(qū):Preverse Log 可以在面板中保留網(wǎng)絡(luò)請(qǐng)求,在頁(yè)面重定向、當(dāng)頁(yè)跳轉(zhuǎn)時(shí)可以保留之前頁(yè)面的日志;Disable Cache可以屏蔽瀏覽器的 http 緩存機(jī)制;右側(cè)的 No throtting 選擇器可以對(duì)當(dāng)前網(wǎng)絡(luò)狀態(tài)進(jìn)行模擬(Fast 3G、Slow 3G 等等)。
- 請(qǐng)求列表、請(qǐng)求狀態(tài)。
- 請(qǐng)求傳輸體積:默認(rèn)展示 gzip/br 壓縮后的大小。
- Waterfall:資源加載的時(shí)序和每一步的耗時(shí)。
Performance
Performance 面板可以提供更加專業(yè)的性能信息。
WebPageTest
?WebPageTest是一個(gè)線上性能分析平臺(tái),除了常用的 cwv 性能數(shù)據(jù)外,還有 performance、lighthouse 報(bào)告、頁(yè)面對(duì)比等功能。
輸入 URL 后我們可以簡(jiǎn)單的選擇一個(gè) simple configuration 進(jìn)行測(cè)試,默認(rèn)會(huì)執(zhí)行 3 次測(cè)試。這里我們可以看到頁(yè)面的一些核心指標(biāo),可以點(diǎn)開對(duì)應(yīng)的指標(biāo)項(xiàng)進(jìn)行更詳細(xì)的分析,在 Waterfall 頁(yè)面我們也可以很直觀的看到當(dāng)前頁(yè)面的請(qǐng)求順序、請(qǐng)求耗時(shí)、關(guān)鍵節(jié)點(diǎn)(FCP、LCP等等)。
優(yōu)化手段
網(wǎng)絡(luò)傳輸優(yōu)化
這里我們著重看三個(gè)時(shí)間指標(biāo):
- Total Connection Time:整體的連接耗時(shí)
- TTFB(Time to First Byte):首字節(jié)傳輸耗時(shí)
- Content Download:內(nèi)容傳輸耗時(shí)
Total Connection Time
導(dǎo)致連接耗時(shí)長(zhǎng)的因素可能會(huì)有很多種:
- 機(jī)器距離用戶端的物理距離過長(zhǎng)(美國(guó) - 中國(guó))。
- 重復(fù)建聯(lián),在頁(yè)面中使用了多個(gè)不同域名,每次都需要重新建立連接。
- 用戶端網(wǎng)絡(luò)環(huán)境問題。
那么為了解決這些問題我們可以采取哪些手段呢:
- 利用 CDN 對(duì)主域名進(jìn)行動(dòng)態(tài)加速,對(duì)資源域名進(jìn)行緩存,利用邊緣節(jié)點(diǎn)的特性縮短用戶請(qǐng)求距離。
- 利用 pre-connect 對(duì)域名進(jìn)行預(yù)建聯(lián),同時(shí)對(duì)域名進(jìn)行收攏,這樣在 http2 的情況下可以減少建聯(lián)耗時(shí)。
- 充分利用 http 緩存和 servicesworker 請(qǐng)求攔截的特性,對(duì)可緩存的資源進(jìn)行本地緩存,減少發(fā)起網(wǎng)絡(luò)請(qǐng)求次數(shù)。
TTFB + Content Download
TTFB 是從發(fā)起請(qǐng)求到收到服務(wù)器請(qǐng)求第一個(gè)字節(jié)的時(shí)間,一般來說,如果首屏 html 請(qǐng)求的 TTFB 能達(dá)到 100ms 以內(nèi),就已經(jīng)具備不錯(cuò)的體驗(yàn)了,如果超過了 500ms,那么用戶就能明顯的感受到白屏,精準(zhǔn)的來說,TTFB 是在完成 DNS 查詢、TCP 握手、SSL 握手后發(fā)起 HTTP 請(qǐng)求報(bào)文到接收到服務(wù)端第一個(gè)響應(yīng)報(bào)文的時(shí)間差,大約等于 一個(gè)RTT(Round-Trip Time 即往返時(shí)延)+ ServerRT。
那么當(dāng) TTFB 耗時(shí)很長(zhǎng)時(shí),如何進(jìn)行優(yōu)化呢?可以參考如下幾種方式:
- 減少請(qǐng)求傳輸量,避免無用信息。
- 減少服務(wù)端處理時(shí)間(增加緩存、慢 SQL 治理等等)。
- 對(duì)首屏 HTML 內(nèi)容做流式渲染,由于瀏覽器對(duì) HTML 的解析并不依賴與下載完整的 HTML,而是解析一部分渲染一部分,所以服務(wù)端可以先將部分準(zhǔn)備好的內(nèi)容通過流式渲染的方式返回,而不是等全部?jī)?nèi)容就緒后再返回。
- 懶加載:優(yōu)先返回必要內(nèi)容,例如超長(zhǎng)頁(yè)面,可以先返回首屏看到的內(nèi)容,剩下的通過異步加載的方式進(jìn)行渲染,分多個(gè)接口進(jìn)行請(qǐng)求。
那么,是 TTFB 越短越好嗎?
其實(shí)也不盡然,我們需要做好 TTFB 和 Content Download 的權(quán)衡,例如當(dāng)我們開啟 gzip/br 壓縮的時(shí)候,TTFB 必然會(huì)呈上漲趨勢(shì),但是相對(duì)應(yīng)的資源體積變小,就會(huì)加快傳輸耗時(shí),減少 Content Download 時(shí)間,所以我們應(yīng)該關(guān)注的用戶真實(shí)的體驗(yàn),而不是一味地盯著時(shí)間進(jìn)行優(yōu)化。
preload
preload 也就是預(yù)加載,關(guān)于預(yù)加載的方式有很多種,端內(nèi)和端外也各自有不同的方案,比較常見的有:
- ?preload 標(biāo)簽:
- serviceworker 預(yù)加載:flasher、workbox-preload 等。
- zcache:在客戶端端內(nèi)通過資源離線包的方式進(jìn)行預(yù)加載。
相關(guān)鏈接
?Lighthouse Scoring Calculator?:
https://googlechrome.github.io/lighthouse/scorecalc/
?WebPageTest?:
https://www.webpagetest.org/
?Web Vitals?:
https://web.dev/vitals/
?web-vitals - Github?:
https://github.com/GoogleChrome/web-vitals