項目性能優化實踐:深入FMP算法原理探索
一、前言
二、什么是FMP
三、FMP計算原理
1. 核心思想
2. FMP的三種計算方式
3. 新算法vs傳統算法
4. 關鍵算法
四、時間標記機制
1. DOM變化監聽
2. 時間標記
3. 標記值獲取
五、資源加載考慮
1. 資源類型識別
2. 資源時間獲取
3. 綜合時間計算
六、子頁面支持
1. 時間偏移處理
2. FMP值調整
七、FMP的核心優勢
1. 用戶感知導向
2. 多維度計算體系
3. 高精度測量
八、FMP的實際應用場景
1. 性能監控實踐
2. 用戶體驗評估
3. 優化指導價值
九、總結
一、前言
最近在項目中遇到了頁面加載速度優化的問題,為了提高秒開率等指標,我決定從eebi報表入手,分析一下當前項目的性能監控體系。
通過查看報表中的cost_time、is_first等字段,我開始了解項目的性能數據采集情況。為了更好地理解這些數據的含義,我深入研究了相關SDK的源碼實現。
在分析過程中,我發現采集到的cost_time參數實際上就是FMP(First Meaningful Paint)指標。于是我對FMP的算法實現進行了梳理,了解了它的計算邏輯。
本文將分享我在性能優化過程中的一些思考和發現,希望能對關注前端性能優化的同學有所幫助。
二、什么是FMP
FMP (First Meaningful Paint) 首次有意義繪制,是指頁面首次繪制有意義內容的時間點。與 FCP (First Contentful Paint) 不同,FMP 更關注的是對用戶有實際價值的內容,而不是任何內容的首次繪制。
三、FMP 計算原理
核心思想
FMP 的核心思想是:通過分析視口內重要 DOM 元素的渲染時間,找到對用戶最有意義的內容完成渲染的時間點。
FMP的三種計算方式
- 新算法 FMP (specifiedValue)
a.基于用戶指定的 DOM 元素計算
b.通過fmpSelector配置指定元素
c.計算指定元素的完整加載時間
- 傳統算法 FMP (value)
a.基于視口內重要元素計算
b.選擇權重最高的元素
c.取所有參考元素中最晚完成的時間
- P80 算法 FMP (p80Value)
a.基于 P80 百分位計算
b.取排序后80%位置的時間
c.更穩定的性能指標
新算法vs傳統算法
傳統算法流程
- 遍歷整個DOM樹
- 計算每個元素的權重分數
- 選擇多個重要元素
- 計算所有元素的加載時間
- 取最晚完成的時間作為FMP
新算法(指定元素算法)流程
核心思想:直接指定一個關鍵 DOM 元素,計算該元素的完整加載時間作為FMP。
傳統算法詳細步驟
第一步:DOM元素選擇
// 遞歸遍歷 DOM 樹,選擇重要元素
selectMostImportantDOMs(dom: HTMLElement = document.body): void {
const score = this.getWeightScore(dom);
if (score > BODY_WEIGHT) {
// 權重大于 body 權重,作為參考元素
this.referDoms.push(dom);
} else if (score >= this.highestWeightScore) {
// 權重大于等于最高分數,作為重要元素
this.importantDOMs.push(dom);
}
// 遞歸處理子元素
for (let i = 0, l = dom.children.length; i < l; i++) {
this.selectMostImportantDOMs(dom.children[i] as HTMLElement);
}
}第二步:權重計算
// 計算元素權重分數
getWeightScore(dom: Element) {
// 獲取元素在視口中的位置和大小
const viewPortPos = dom.getBoundingClientRect();
const screenHeight = this.getScreenHeight();
// 計算元素在首屏中的可見面積
const fpWidth = Math.min(viewPortPos.right, SCREEN_WIDTH) - Math.max(0, viewPortPos.left);
const fpHeight = Math.min(viewPortPos.bottom, screenHeight) - Math.max(0, viewPortPos.top);
// 權重 = 可見面積 × 元素類型權重
return fpWidth * fpHeight * getDomWeight(dom);
}權重計算公式:
權重分數 = 可見面積 × 元素類型權重元素類型權重:
- OBJECT, EMBED, VIDEO: 最高權重
- SVG, IMG, CANVAS: 高權重
- 其他元素: 權重為 1
第三步:加載時間計算
getLoadingTime(dom: HTMLElement, resourceLoadingMap: Record<string, any>): number {
// 獲取 DOM 標記時間
const baseTime = getMarkValueByDom(dom);
// 獲取資源加載時間
let resourceTime = 0;
if (RESOURCE_TAG_SET.indexOf(tagType) >= 0) {
// 處理圖片、視頻等資源
const resourceTiming = resourceLoadingMap[resourceName];
resourceTime = resourceTiming ? resourceTiming.responseEnd : 0;
}
// 返回較大值(DOM 時間 vs 資源時間)
return Math.max(resourceTime, baseTime);
}第四步:FMP值計算
calcValue(resourceLoadingMap: Record<string, any>, isSubPage: boolean = false): void {
// 構建參考元素列表(至少 3 個元素)
const referDoms = this.referDoms.length >= 3
? this.referDoms
: [...this.referDoms, ...this.importantDOMs.slice(this.referDoms.length - 3)];
// 計算每個元素的加載時間
const timings = referDoms.map(dom => this.getLoadingTime(dom, resourceLoadingMap));
// 排序時間數組
const sortedTimings = timings.sort((t1, t2) => t1 - t2);
// 計算最終值
const info = getMetricNumber(sortedTimings);
this.value = info.value; // 最后一個元素的時間(最晚完成)
this.p80Value = info.p80Value; // P80 百分位時間
}新算法詳細步驟
第一步:配置指定元素
// 通過全局配置指定 FMP 目標元素
const { fmpSelector = "" } = SingleGlobal?.getOptions?.();配置示例:
// 初始化時配置
init({
fmpSelector: '.main-content', // 指定主要內容區域
// 或者
fmpSelector: '#hero-section', // 指定首屏區域
// 或者
fmpSelector: '.product-list' // 指定產品列表
});第二步:查找指定元素
if (fmpSelector) {
// 使用 querySelector 查找指定的 DOM 元素
const $specifiedEl = document.querySelector(fmpSelector);
if ($specifiedEl && $specifiedEl instanceof HTMLElement) {
// 找到指定元素,進行后續計算
this.specifiedDom = $specifiedEl;
}
}查找邏輯:
- 使用document.querySelector()查找元素
- 驗證元素存在且為 HTMLElement 類型
- 保存元素引用到specifiedDom
第三步:計算指定元素的加載時間
// 計算指定元素的完整加載時間
this.specifiedValue = this.getLoadingTime(
$specifiedEl,
resourceLoadingMap
);加載時間計算包含:
- DOM 標記時間
// 獲取 DOM 元素的基礎標記時間
const baseTime = getMarkValueByDom(dom);- 資源加載時間
let resourceTime = 0;
// 處理直接資源(img, video, embed 等)
const tagType = dom.tagName.toUpperCase();
if (RESOURCE_TAG_SET.indexOf(tagType) >= 0) {
const resourceName = normalizeResourceName((dom as any).src);
const resourceTiming = resourceLoadingMap[resourceName];
resourceTime = resourceTiming ? resourceTiming.responseEnd : 0;
}
// 處理背景圖片
const bgImgUrl = getDomBgImg(dom);
if (isImageUrl(bgImgUrl)) {
const resourceName = normalizeResourceName(bgImgUrl);
const resourceTiming = resourceLoadingMap[resourceName];
resourceTime = resourceTiming ? resourceTiming.responseEnd : 0;
}- 綜合時間計算
// 返回 DOM 時間和資源時間的較大值
return Math.max(resourceTime, baseTime);第四步:FMP值確定
// 根據是否有指定值來決定使用哪個 FMP 值
if (specifiedValue === 0) {
// 如果沒有指定值,回退到傳統算法
fmp = isSubPage ? value - diffTime : value;
} else {
// 如果有指定值,使用指定值
fmp = isSubPage ? specifiedValue - diffTime : specifiedValue;
}決策邏輯:
- 如果 specifiedValue > 0:使用指定元素的加載時間
- 如果 specifiedValue === 0:回退到傳統算法
第五步:子頁面時間調整
// 子頁面的 FMP 值需要減去時間偏移
if (isSubPage) {
fmp = specifiedValue - diffTime;
// diffTime = startSubTime - initTime
}新算法的優勢
精確性更高
- 直接針對業務關鍵元素
- 避免權重計算的誤差
- 更貼近業務需求
可控性強
- 開發者可以指定關鍵元素
- 可以根據業務場景調整
- 避免算法自動選擇的偏差
計算簡單
- 只需要計算一個元素
- 不需要復雜的權重計算
- 性能開銷更小
業務導向
- 直接反映業務關鍵內容的加載時間
- 更符合用戶體驗評估需求
- 便于性能優化指導
關鍵算法
P80 百分位計算
export function getMetricNumber(sortedTimings: number[]) {
const value = sortedTimings[sortedTimings.length - 1]; // 最后一個(最晚)
const p80Value = sortedTimings[Math.floor((sortedTimings.length - 1) * 0.8)]; // P80
return { value, p80Value };
}元素類型權重
const IMPORTANT_ELEMENT_WEIGHT_MAP = {
SVG: IElementWeight.High, // 高權重
IMG: IElementWeight.High, // 高權重
CANVAS: IElementWeight.High, // 高權重
OBJECT: IElementWeight.Highest, // 最高權重
EMBED: IElementWeight.Highest, // 最高權重
VIDEO: IElementWeight.Highest // 最高權重
};四、時間標記機制
DOM變化監聽
// MutationObserver 監聽 DOM 變化
private observer = new MutationObserver((mutations = []) => {
const now = Date.now();
this.handleChange(mutations, now);
});時間標記
// 為每個 DOM 變化創建性能標記
mark(count); // 創建 performance.mark(`mutation_pc_${count}`)
// 為 DOM 元素設置標記
setDataAttr(elem, TAG_KEY, `${mutationCount}`);標記值獲取
// 根據 DOM 元素獲取標記時間
getMarkValueByDom(dom: HTMLElement) {
const markValue = getDataAttr(dom, TAG_KEY);
return getMarkValue(parseInt(markValue));
}五、資源加載考慮
資源類型識別
圖片資源: <img> 標簽的 src屬性
視頻資源: <video> 標簽的 src屬性
背景圖片: CSS background-image屬性
嵌入資源: <embed>, <object>標簽
資源時間獲取
// 從 Performance API 獲取資源加載時間
const resourceTiming = resourceLoadingMap[resourceName];
const resourceTime = resourceTiming ? resourceTiming.responseEnd : 0;綜合時間計算
// DOM 時間和資源時間的較大值
return Math.max(resourceTime, baseTime);六、子頁面支持
時間偏移處理
// 子頁面從調用 send 方法開始計時
const diffTime = this.startSubTime - this.initTime;
// 子頁面只統計開始時間之后的資源
if (!isSubPage || resource.startTime > diffTime) {
resourceLoadingMap[resourceName] = resource;
}FMP值調整
// 子頁面的 FMP 值需要減去時間偏移
fmp = isSubPage ? value - diffTime : value;七、FMP的核心優勢
用戶感知導向
FMP 最大的優勢在于它真正關注用戶的實際體驗:
- 內容價值優先:只計算對用戶有意義的內容渲染時間
- 智能權重評估:根據元素的重要性和可見性進行差異化計算
- 真實體驗映射:更貼近用戶的實際感知,而非技術層面的指標
多維度計算體系
FMP 采用了更加全面的計算方式:
- 元素權重分析:綜合考慮元素類型和渲染面積的影響
- 資源加載關聯:將靜態資源加載時間納入計算范圍
- 算法對比驗證:支持多種算法并行計算,確保結果準確性
高精度測量
FMP 在測量精度方面表現突出:
- DOM 變化追蹤:基于實際 DOM 結構變化的時間點
- API 數據融合:結合 Performance API 提供的詳細數據
- 統計分析支持:支持 P80 百分位等多種統計指標,便于性能分析
八、FMP的實際應用場景
性能監控實踐
FMP 在性能監控中發揮著重要作用:
- 關鍵指標追蹤:實時監控頁面首次有意義內容的渲染時間
- 瓶頸識別:快速定位性能瓶頸和潛在的優化點
- 趨勢分析:通過歷史數據了解性能變化趨勢
用戶體驗評估
FMP 為產品團隊提供了用戶視角的性能評估:
- 真實感知測量:評估用戶實際感受到的頁面加載速度
- 競品對比分析:對比不同頁面或產品的性能表現
- 用戶滿意度關聯:將技術指標與用戶滿意度建立關聯
優化指導價值
FMP 數據為性能優化提供了明確的方向:
- 資源優化策略:指導靜態資源加載順序和方式的優化
- 渲染路徑優化:幫助優化關鍵渲染路徑,提升首屏體驗
- 量化效果評估:為優化效果提供可量化的評估標準
九、總結
通過這次深入分析,我對 FMP 有了更全面的認識。FMP 通過科學的算法設計,能夠準確反映用戶感知的頁面加載性能,是前端性能監控的重要指標。
它不僅幫助我們更好地理解頁面加載過程,更重要的是為性能優化提供了科學的依據。在實際項目中,合理運用 FMP 指標,能夠有效提升用戶體驗,實現真正的"秒開"效果。
希望這篇文章能對正在關注前端性能優化的同學有所幫助,也歡迎大家分享自己的實踐經驗。































