一、前 言
最近在项目中遇到了页面加载速度优化的问题,为了提高秒开率等指标,我决定从 eebi 报表入手,分析一下当前项目的性能监控体系。
通过查看报表中的 cost_time、is_first 等字段,我开始了解项目的性能数据采集情况。为了更好地理解这些数据的含义,我深入研究了相关 SDK 的源码实现。
在分析过程中,我发现采集到的 cost_time 参数实际上就是 FMP(First Meaningful Paint)指标。于是我对 FMP 的算法实现进行了梳理,了解了它的计算逻辑。
本文将分享我在性能优化过程中的一些思考和发现,希望能对关注前端性能优化的同学有所帮助。
二、什么是 FMP
FMP (First Meaningful Paint) 首次有意义绘制,是指页面首次绘制有意义内容的时间点。与 FCP (First Contentful Paint) 不同,FMP 更关注的是对用户有实际价值的内容,而不是任何内容的首次绘制。
三、FMP 计算原理
3.1 核心思想
FMP 的核心思想是:通过分析视口内重要 DOM 元素的渲染时间,找到对用户最有意义的内容完成渲染的时间点。
3.2FMP 的三种计算方式
3.3 新算法 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);}
复制代码
权重计算公式:
元素类型权重:
第三步:加载时间计算
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; }}
复制代码
查找逻辑:
第三步:计算指定元素的加载时间
// 计算指定元素的完整加载时间this.specifiedValue = this.getLoadingTime( $specifiedEl, resourceLoadingMap);
复制代码
加载时间计算包含:
// 获取 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;}
复制代码
决策逻辑:
第五步:子页面时间调整
// 子页面的 FMP 值需要减去时间偏移if (isSubPage) { fmp = specifiedValue - diffTime; // diffTime = startSubTime - initTime}
复制代码
新算法的优势
精确性更高
直接针对业务关键元素
避免权重计算的误差
更贴近业务需求
可控性强
开发者可以指定关键元素
可以根据业务场景调整
避免算法自动选择的偏差
计算简单
只需要计算一个元素
不需要复杂的权重计算
性能开销更小
业务导向
直接反映业务关键内容的加载时间
更符合用户体验评估需求
便于性能优化指导
3.4 关键算法
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 // 最高权重};
复制代码
四、时间标记机制
4.1DOM 变化监听
// MutationObserver 监听 DOM 变化private observer = new MutationObserver((mutations = []) => { const now = Date.now(); this.handleChange(mutations, now);});
复制代码
4.2 时间标记
// 为每个 DOM 变化创建性能标记mark(count); // 创建 performance.mark(`mutation_pc_${count}`)// 为 DOM 元素设置标记setDataAttr(elem, TAG_KEY, `${mutationCount}`);
复制代码
4.3 标记值获取
// 根据 DOM 元素获取标记时间getMarkValueByDom(dom: HTMLElement) { const markValue = getDataAttr(dom, TAG_KEY); return getMarkValue(parseInt(markValue));}
复制代码
五、资源加载考虑
5.1 资源类型识别
图片资源: <img> 标签的 src 属性
视频资源: <video> 标签的 src 属性
背景图片: CSS background-image 属性
嵌入资源: <embed>, <object>标签
5.2 资源时间获取
// 从 Performance API 获取资源加载时间const resourceTiming = resourceLoadingMap[resourceName];const resourceTime = resourceTiming ? resourceTiming.responseEnd : 0;
复制代码
5.3 综合时间计算
// DOM 时间和资源时间的较大值return Math.max(resourceTime, baseTime);
复制代码
六、子页面支持
6.1 时间偏移处理
// 子页面从调用 send 方法开始计时const diffTime = this.startSubTime - this.initTime;// 子页面只统计开始时间之后的资源if (!isSubPage || resource.startTime > diffTime) { resourceLoadingMap[resourceName] = resource;}
复制代码
6.2FMP 值调整
// 子页面的 FMP 值需要减去时间偏移fmp = isSubPage ? value - diffTime : value;
复制代码
七、FMP 的核心优势
7.1 用户感知导向
FMP 最大的优势在于它真正关注用户的实际体验:
7.2 多维度计算体系
FMP 采用了更加全面的计算方式:
7.3 高精度测量
FMP 在测量精度方面表现突出:
DOM 变化追踪:基于实际 DOM 结构变化的时间点
API 数据融合:结合 Performance API 提供的详细数据
统计分析支持:支持 P80 百分位等多种统计指标,便于性能分析
八、FMP 的实际应用场景
8.1 性能监控实践
FMP 在性能监控中发挥着重要作用:
8.2 用户体验评估
FMP 为产品团队提供了用户视角的性能评估:
真实感知测量:评估用户实际感受到的页面加载速度
竞品对比分析:对比不同页面或产品的性能表现
用户满意度关联:将技术指标与用户满意度建立关联
8.3 优化指导价值
FMP 数据为性能优化提供了明确的方向:
资源优化策略:指导静态资源加载顺序和方式的优化
渲染路径优化:帮助优化关键渲染路径,提升首屏体验
量化效果评估:为优化效果提供可量化的评估标准
九、总结
通过这次深入分析,我对 FMP 有了更全面的认识。FMP 通过科学的算法设计,能够准确反映用户感知的页面加载性能,是前端性能监控的重要指标。
它不仅帮助我们更好地理解页面加载过程,更重要的是为性能优化提供了科学的依据。在实际项目中,合理运用 FMP 指标,能够有效提升用户体验,实现真正的"秒开"效果。
希望这篇文章能对正在关注前端性能优化的同学有所帮助,也欢迎大家分享自己的实践经验。
往期回顾
1. Dragonboat 统一存储 LogDB 实现分析|得物技术
2. 从数字到版面:得物数据产品里数字格式化的那些事
3. 一文解析得物自建 Redis 最新技术演进
4. Golang HTTP 请求超时与重试:构建高可靠网络请求|得物技术
5. RN 与 hawk 碰撞的火花之 C++异常捕获|得物技术
文 /阿列
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
评论