什么是FMP
FMP (First Meaningful Paint) 首次有意义绘制,是指页面首次绘制对用户有实际价值内容的时间点。它与 FCP (First Contentful Paint) 不同,FCP 记录的是任何内容的首次绘制,而 FMP 更关注那些“有意义”内容的渲染完成时刻。
FMP 计算原理
核心思想
FMP 的核心思想是:通过分析视口内重要 DOM 元素的渲染时间,找到对用户最有意义的内容完成渲染的时间点。
FMP的三种计算方式
- 新算法 FMP (specifiedValue):基于用户指定的 DOM 元素计算。通过
fmpSelector 配置指定关键元素,计算该元素的完整加载时间。这在复杂的前端工程化项目中有助于精确监控核心模块。
- 传统算法 FMP (value):基于视口内重要元素计算。算法会自动选择权重最高的元素,并取所有参考元素中最晚完成的时间作为指标。
- P80 算法 FMP (p80Value):基于 P80 百分位计算。取排序后80%位置的时间,旨在提供一个更稳定、更能抵抗异常值影响的性能指标。
新算法 vs 传统算法
传统算法流程:
- 递归遍历整个DOM树。
- 根据元素在视口中的面积和类型权重计算每个元素的分数。
- 选择多个重要元素作为参考。
- 计算这些元素的最终加载时间(综合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: 高权重
- 其他元素: 基础权重
第三步:加载时间计算
获取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值计算
综合参考元素的加载时间,计算出最终的传统算法值和P80值。
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 百分位时间
}
新算法详细步骤
第一步:配置指定元素
在SDK初始化时,通过配置项指定CSS选择器。
// 初始化时配置
init({
fmpSelector: '.main-content', // 指定主要内容区域
});
第二步:查找指定元素
在页面中使用 querySelector 查找配置的元素。
if (fmpSelector) {
// 使用 querySelector 查找指定的 DOM 元素
const $specifiedEl = document.querySelector(fmpSelector);
if ($specifiedEl && $specifiedEl instanceof HTMLElement) {
// 找到指定元素,进行后续计算
this.specifiedDom = $specifiedEl;
}
}
第三步:计算指定元素的加载时间
计算逻辑与传统算法中单元素的加载时间计算一致,综合了DOM变化标记和资源加载。
- DOM 标记时间:通过监听
DOM 变化获得。
- 资源加载时间:通过
Performance API 获取关联的图片、视频等资源的 responseEnd 时间。
- 综合时间:取上述两者中的较大值作为该元素的最终加载时间。
第四步:FMP值确定
决策逻辑简单直接:如果指定元素计算出的 specifiedValue > 0,则使用该值;否则回退到传统算法计算出的 value。
第五步:子页面时间调整
对于单页应用(SPA)中的子页面,FMP 值需要减去页面初始化到子页面开始加载的时间偏移量,以确保时间基准一致。
新算法的优势
- 精确性更高:直接针对业务关键元素,避免自动权重计算可能产生的偏差。
- 可控性强:开发者可根据业务场景指定监控目标,使性能指标与业务价值强关联。
- 计算简单:只需监控和计算一个元素,性能开销小。
- 业务导向:指标直接反映核心内容的加载体验,便于评估和优化。
关键算法
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 };
}
时间标记机制
FMP 计算依赖精确的 DOM 渲染时间点,这是通过时间标记机制实现的。
DOM变化监听
使用 MutationObserver API 监听整个文档的 DOM 变化。
// MutationObserver 监听 DOM 变化
private observer = new MutationObserver((mutations = []) => {
const now = Date.now();
this.handleChange(mutations, now);
});
时间标记
每当监听到有效的 DOM 变化(如添加了可见元素),就创建一个唯一的时间戳标记,并将其关联到新增的 DOM 元素上。
// 为每个 DOM 变化创建性能标记
mark(count); // 创建 performance.mark(`mutation_pc_${count}`)
// 为 DOM 元素设置标记
setDataAttr(elem, TAG_KEY, `${mutationCount}`);
标记值获取
计算 FMP 时,通过获取元素上关联的标记索引,再查询对应的 performance.mark 时间,即可得到该元素被渲染的时间点。
// 根据 DOM 元素获取标记时间
getMarkValueByDom(dom: HTMLElement) {
const markValue = getDataAttr(dom, TAG_KEY);
return getMarkValue(parseInt(markValue));
}
资源加载考虑
有意义的元素往往依赖图片、视频等资源,FMP 算法将这些资源的加载完成时间也纳入了考量。
资源类型识别
- 图片资源:
<img> 标签。
- 视频资源:
<video> 标签。
- 背景图片: 通过
getComputedStyle 获取的 background-image。
- 嵌入资源:
<embed>, <object> 标签。
资源时间获取
通过 performance.getEntriesByType('resource') 获取所有资源的加载性能时序数据,并建立 URL 到 responseEnd 时间的映射。
// 从 Performance API 获取资源加载时间
const resourceTiming = resourceLoadingMap[resourceName];
const resourceTime = resourceTiming ? resourceTiming.responseEnd : 0;
综合时间计算
元素的最终“加载完成时间”是其自身 DOM 渲染时间和其关联资源加载时间的最大值。
// DOM 时间和资源时间的较大值
return Math.max(resourceTime, baseTime);
子页面支持
为了在单页应用(SPA)中准确衡量每个子页面的性能,FMP 算法做了特殊处理。
时间偏移处理
子页面的性能监控从特定的路由跳转或内容更新时开始。算法会记录一个初始时间偏移量,并只统计此偏移量之后加载的资源,确保数据纯净。
// 子页面从调用 send 方法开始计时
const diffTime = this.startSubTime - this.initTime;
// 子页面只统计开始时间之后的资源
if (!isSubPage || resource.startTime > diffTime) {
resourceLoadingMap[resourceName] = resource;
}
FMP值调整
计算出的子页面 FMP 值需要减去初始的时间偏移量,从而得到相对于子页面开始加载时刻的真实耗时。
// 子页面的 FMP 值需要减去时间偏移
fmp = isSubPage ? value - diffTime : value;
FMP的核心优势
- 用户感知导向:FMP 的核心在于捕捉用户真正“看到”主要内容的那一刻,而非技术上的首次绘制。它通过评估元素的重要性(面积、类型)来逼近用户的视觉焦点,使性能指标与用户体验紧密挂钩。理解这种以用户为中心的度量思路,对于构建任何高性能的前端框架/工程化应用都至关重要。
- 多维度计算体系:算法并非单一维度。它结合了 DOM 结构变化、CSS 渲染、静态资源加载、以及基于视口的几何计算,形成了一个综合评估体系,能够更全面地反映页面加载的实际状况。
- 高精度测量:基于
MutationObserver 和 Performance Timeline API 进行毫秒级的时间标记和数据采集,并通过 P80 等统计方法平滑异常值,确保了测量结果的准确性和稳定性。
FMP的实际应用场景
- 性能监控实践:FMP 是前端性能监控的核心指标之一。通过持续采集和分析 FMP 数据,可以建立性能基线,快速发现性能劣化,定位瓶颈是在网络请求、资源加载还是DOM渲染阶段。
- 用户体验评估:产品运营和用户体验团队可以将 FMP 数据与用户行为数据(如跳出率、转化率)关联分析,量化页面加载速度对业务指标的实际影响,为优化决策提供数据支持。
- 优化指导价值:FMP 的详细计算逻辑直接指出了优化方向。例如,若算法选中的关键元素包含大图,则优化图片加载(懒加载、压缩、CDN)将直接提升 FMP 指标。这种从监控到诊断再到优化的闭环,是高效运维/DevOps文化在前端领域的体现。
总结
深入理解 FMP 算法,不仅让我们知道如何解读一个性能指标的数字,更让我们洞悉前端页面从加载到渲染的完整过程。传统算法提供了自动化评估的通用方案,而新算法(指定元素)则赋予了开发者针对关键业务路径进行精准监控的能力。在实际项目中,结合业务形态选择合适的监控策略,并利用 FMP 提供的洞察持续优化,才能有效提升用户体验,实现真正的性能卓越。