什麼是FMP?
可能大家對「白屏時間」這個名詞並不陌生,他是「刀耕火種」年代,我們收集的頁面效能指標之一,隨著前端工程的複雜化,白屏時間已經沒有什麼實質性的意義了,取而代之的就是 FMP。
先來介紹幾個與之相關的名詞。
- FP(First Paint):首次繪製,標記瀏覽器渲染任何在視覺上不同於導航前螢幕內容的時間點
- FCP(First Contentful Paint):首次內容繪製,標記的是瀏覽器渲染第一針內容 DOM 的時間點,該內容可能是文字、影象、SVG 或者
<canvas>
等元素 - FMP(First Meaning Paint):首次有效繪製,標記主角元素渲染完成的時間點,主角元素可以是視訊網站的視訊控制元件,內容網站的頁面框架也可以是資源網站的頭圖等。
相對於 FP 和 FCP,FMP 是我們前端最常關注的重要效能指標,Google 定義它為「是否有用?」的時間點。然而,「是否有用?」是很難以通用方式界定的,因此,至今依然沒有標準的 API 輸出。
社群中常有這麼幾種方式進行「相對準確」的計算 FMP,所謂相對準確,是相對於實際專案而言。
- 主動上報:開發者在相應頁面的「Meaning」位置上報時間
- 權重計算:根據頁面元素,計算權重最高的元素渲染時間
- 趨勢計算:在 render 期間,根據 dom 的變化趨勢推算 FMP 值
本文將著重介紹第二種方式。
權重定位
所謂權重,即,將頁面的元素以約定的「權重比」遍歷出「權重值」最大的某一個或一組 DOM,然後以其「裝載時間點」或「載入結束點」作為 FMP 的對映。
權重計算
節點標記
想要對 DOM 節點進行階段性標記,就得有監聽 DOM 變化的能力,慶幸的是,HTML5 賦予了我們這個能力。
MutationObserver
,Mutation Events功能的替代品,是DOM3 Events規範的一部分。他可以在指定的 DOM 發生變化時執行回撥。
MutationObserver 有三個方法
-
disconnect()
阻止 MutationObserver 例項繼續接收的通知,直到再次呼叫其observe()方法,該觀察者物件包含的回撥函式都不會再被呼叫。
-
observe()
配置MutationObserver在DOM更改匹配給定選項時,通過其回撥函式開始接收通知。
-
takeRecords()
從MutationObserver的通知佇列中刪除所有待處理的通知,並將它們返回到MutationRecord物件的新Array中。
global.mo = new MutationObserver(() => {
/* callback: DOM 節點設定階段性標記 */
});
/**
* mutationObserver.observe(target[, options])
* target - 需要觀察變化的 DOM Node。
* options - MutationObserverInit 物件,配置需要觀察的變化項。
* 更多 options 的介紹請參考 https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserverInit#%E5%B1%9E%E6%80%A7
**/
global.mo.observe(document, {
childList: true, // 監聽子節點變化(如果subtree為true,則包含子孫節點)
subtree: true // 整個子樹的所有節點
});
複製程式碼
下圖粗濾的解析了正常單頁面的渲染過程
- 預備階段:導航階段,處在連線相應的過程
- 階段一:首位元組渲染階段,也是FCP,DOM 樹的第一次有效變化
- 階段二:基本框架渲染完成
- 階段三:獲取到資料,渲染到檢視上
- 階段四:圖片載入完成,載入過程不被標記
實際上在第一、第三階段之間還存在著大量的 DOM 變化,Mutation Observer 事件的觸發並不是同步的,而是非同步觸發的,也就是說,等到當前「階段」所有 DOM 操作都結束才觸發。
Mutation Observer 有以下特點
- 它等待所有指令碼任務完成後,才會執行(即非同步觸發方式)。
- 它把 DOM 變動記錄封裝成一個陣列進行處理,而不是一條條個別處理 DOM 變動。
- 它既可以觀察 DOM 的所有型別變動,也可以指定只觀察某一類變動。
在 load
事件觸發後,各個階段的 tag 已經被打到標籤上了
此處以『_ti
』昨晚標記 key。
在打標記的同時,需要記錄下當前的時間節點,備用
// 虛擬碼
function callback() {
global.timeStack[++_ti] = performance.now(); // 記時間
doTag(_ti); // 打標記
}
複製程式碼
標記打完後就等 load 的那一刻進行計算反推了。
計算權重值
一般來說
- 檢視佔比越大的元素越有可能是主角元素
- 視訊比圖片更可能是主角元素
svg
和canvas
也很重要- 其他元素都可以按普通 dom 計算了
- 背景圖片事情可認定
第一步:簡單粗暴,按大小計算
// 虛擬碼
function weightCompute(node){
let {
width,
height,
left,
top
} = node.getBoundingClientRect();
// 排除檢視外的元素
if(isOutside(width, height, left, top)){
return 0;
}
let wts = TAG_WEIGHT_MAP[node.tagName]; // 約定好的權重比
let weight = width * height * wts; // 直接乘,或者更細粒度的計算 wts(width, height, wts)
return {
weight,
wts,
tagName: node.tagName,
ti: node.getAttribute("_ti"),
node
};
}
複製程式碼
第二步:根據權重值推導主角元素
在我們的約定權重演算法下,權重最大的元素即為我們推到的主角元素。
// 虛擬碼
function getCoreNode(node){
let list = nodeTraversal(node); // 遞迴計算每個標記節點的權重值
return getNodeWithMaxWeight(list); // weight 最大的元素
}
複製程式碼
第三步:根據元素型別取時間
不同的元素獲取時間的方式並不相同
- 普通元素:按標記點時間計算
- 圖片和視訊:按資源相應結束時間計算
- 帶背景元素:可以以背景資源相應結束時間計算,也可以按普通元素計算
// 虛擬碼
function getFMP(){
let coreObj = getCoreNode(document.body),
fmp = -1;
let {
tagName,
ti,
node
} = coreObj;
switch(tagName){
case 'IMG':
case 'VIDEO':
let source = node.src;
let { responseEnd } = performance.getEntries().find(item => item.name === source);
fmp = responseEnd || -1;
break;
default:
if(node.style.backgroundImage){
// 普通元素的背景處理
}else{
fmp = global.timeStack[+ti];
}
}
return fmp;
}
複製程式碼
迴歸驗證
以我們的 demo 頁為例,類似的電商網站,我們希望拿到「階段二」或「階段三」的時間點作為我們的 FMP 值。
因為我們並不希望「主角元素」的背景或者「圖片主角元素」的相應時間算在 FMP 的值內,所以,我們將「圖片」「視訊」等資源元素降級成普通元素計算。
在 Chrome [ Disable cache / Fast 3G ] 條件下我們進行模擬驗證。
計算得到的 FMP 值為 4730.7ms,Chrome Performance 監控的值在 4950ms 左右,誤差在 200ms 左右。
如果將限速放開,FMP 的取值將更接近我們希望的「First Meaning Paint」。
轉載請標明出處
作者: 木羽 zwwill
首發地址:zwwill/blog#32