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