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