得物技术

项目性能优化实践:深入FMP算法原理探索|得物技术

Image

目录

一、前言

二、什么是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)

    • 基于用户指定的 DOM 元素计算

    • 通过fmpSelector配置指定元素

    • 计算指定元素的完整加载时间

  • 传统算法 FMP (value)

    • 基于视口内重要元素计算

    • 选择权重最高的元素

    • 取所有参考元素中最晚完成的时间

  • P80 算法 FMP (p80Value)

    • 基于 P80 百分位计算

    • 取排序后80%位置的时间

    • 更稳定的性能指标

新算法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 指标,能够有效提升用户体验,实现真正的"秒开"效果。

希望这篇文章能对正在关注前端性能优化的同学有所帮助,也欢迎大家分享自己的实践经验。

往期回顾

1. Dragonboat统一存储LogDB实现分析|得物技术

2. 从数字到版面:得物数据产品里数字格式化的那些事

3. 一文解析得物自建 Redis 最新技术演进

4. Golang HTTP请求超时与重试:构建高可靠网络请求|得物技术

5. RN与hawk碰撞的火花之C++异常捕获|得物技术

文 /阿列

关注得物技术,每周一、三更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

“

扫码添加小助手微信

如有任何疑问,或想要了解更多技术资讯,请添加小助手微信:

Image