Skip to Content

面试导航 - 程序员面试题库大全 | 前端后端面试真题 | 面试

前端性能优化NEW前端性能指标--FMP

FMP (First Meaningful Paint) 是 Web 性能优化中的一个重要指标,用于衡量页面在加载过程中,用户看到页面的“首次有意义内容”的时间。具体来说,它表示的是用户在浏览器中看到内容的第一刻——即页面的 可见性 和 可交互性 发生了变化。

FMP 作为衡量页面加载速度的一个关键指标,它专注于页面 可视区域 的内容渲染,而不仅仅是页面的加载完成。FMP 试图衡量的是,用户可以开始看到和与之交互的最早时刻,而不是页面完全加载完成的时刻(例如,当 load 事件触发时)。

换句话说,FMP 让你知道在页面加载过程中,用户可以开始实际感知和使用页面的时间,这对于优化 用户体验 和 互动性 至关重要。

首先我们看看下面的图:

我们可以发现在页面中比较 useful 的内容,都是含有信息量比较丰富的,比如图片,视频,动画,另外就是占可视面积较大的,页面中还存在两种形态的内容可以被视为是 useful 的,一种是单一的块状元素,另外一种是由多个元素组合而成的大元素,比如视频元素,banner 图,这种属于单一的块状元素,而像图片列表,多图像的组合,这种属于元素组合

讨论 FMP,实际上就是回答 is it useful? 这个问题。通常业界会将 FMP 的时间当成是首屏时间,虽然在绝对准确度方面不会相等,但是都可以精准的反映出当前页面的加载和渲染的性能情况,FMP 通常被认为是用户获取到了页面主要信息的时刻,也就是说此时用户的需求是得到了满足的,所以产品通常也会关注 FMP 指标。

总结一下成为 FMP 元素的条件:

  • 体积占比比较大

  • 屏幕内可见占比大

  • 资源加载元素占比更高(img, svg , video , object , embed, canvas)

  • 主要元素可能是多个组成的

算法如何设置

前面介绍了 FMP 的概念还有成为 FMP 的条件,接下来我们来看看如何设计 FMP 获取的算法,按照上面的介绍,我们知道算法分为以下两个部分:

  • 获取 FMP 元素

  • 计算 FMP 元素的加载时间

具体的算法流程如下图:

完整代码如下所示:

interface FMPScore { score: number; elements: HTMLElement[]; } class FMPTiming { private observer: MutationObserver | null = null; private startTime: number; private entries: { time: number; score: number }[] = []; private stopped = false; private timer: number | null = null; // 权重配置 private static readonly WEIGHT_MAP = { IMG: 2, SVG: 2, CANVAS: 4, VIDEO: 4, OBJECT: 4, EMBED: 4, // 其他元素权重为 1 } as const; // 忽略的标签 private static readonly IGNORE_TAGS = new Set([ 'SCRIPT', 'STYLE', 'META', 'HEAD', 'LINK', 'NOSCRIPT', ]); constructor() { this.startTime = performance.now(); this.init(); } public getFMP(): Promise<number> { return new Promise((resolve) => { this.onFMP = resolve; }); } private onFMP: ((time: number) => void) | null = null; private init(): void { // 首次计算 this.calculateScore(); // 观察 DOM 变化 this.observer = new MutationObserver(this.handleMutations); this.observer.observe(document, { childList: true, subtree: true, attributes: true, characterData: true, }); // 设置超时检查 this.timer = window.setTimeout(() => this.stop(), 10000) as unknown as number; // 监听页面加载完成 if (document.readyState === 'complete') { this.checkFMP(); } else { window.addEventListener('load', () => this.checkFMP()); } } private handleMutations = (): void => { if (this.stopped) return; this.calculateScore(); }; private calculateScore(): void { const score = this.getPageScore(); const time = performance.now() - this.startTime; this.entries.push({ time, score: score.score }); // 如果发现分数显著增加,可能就是 FMP 时刻 this.checkFMP(); } private getPageScore(): FMPScore { const elements: HTMLElement[] = []; let totalScore = 0; const walk = (node: HTMLElement) => { if (node.nodeType !== Node.ELEMENT_NODE) return 0; const tagName = node.tagName.toUpperCase(); if (FMPTiming.IGNORE_TAGS.has(tagName)) return 0; // 计算元素分数 const rect = node.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) return 0; // 检查元素是否在视口内 const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; if ( rect.bottom < 0 || rect.right < 0 || rect.top > viewportHeight || rect.left > viewportWidth ) { return 0; } // 计算权重 const weight = FMPTiming.WEIGHT_MAP[tagName as keyof typeof FMPTiming.WEIGHT_MAP] || 1; // 计算元素分数 const score = rect.width * rect.height * weight; // 检查背景图片 const style = window.getComputedStyle(node); const hasBgImage = style.backgroundImage && style.backgroundImage !== 'none'; const finalScore = hasBgImage ? score * 2 : score; elements.push(node); totalScore += finalScore; // 递归计算子元素 for (const child of Array.from(node.children)) { totalScore += walk(child as HTMLElement); } return totalScore; }; walk(document.body); return { score: totalScore, elements }; } private checkFMP(): void { if (this.stopped || this.entries.length < 2) return; const entries = this.entries; let maxIncrease = 0; let fmpTime = 0; // 寻找最大分数增长点 for (let i = 1; i < entries.length; i++) { const increase = entries[i].score - entries[i - 1].score; if (increase > maxIncrease) { maxIncrease = increase; fmpTime = entries[i].time; } } // 如果找到显著变化或页面加载完成,则停止检测 if ( maxIncrease > 0 && (document.readyState === 'complete' || maxIncrease > entries[0].score * 0.5) ) { this.stop(); if (this.onFMP) { this.onFMP(Math.round(fmpTime)); } } } private stop(): void { if (this.stopped) return; this.stopped = true; if (this.observer) { this.observer.disconnect(); this.observer = null; } if (this.timer !== null) { clearTimeout(this.timer); this.timer = null; } } } export default FMPTiming;

在 React 中应该这样使用:

const measurePerformance = async () => { try { const fmpTime = await measureFMP(); console.log('First Meaningful Paint:', fmpTime, 'ms'); // 可以将 FMP 数据发送到你的分析服务 // await sendToAnalytics({ fmp: fmpTime }); } catch (error) { console.error('Failed to measure FMP:', error); } }; useEffect(() => { measurePerformance(); }, []);

接下来我们将对代码进行完整的分析:

  1. 构造函数 constructor

在构造函数中,startTime 用于记录开始时间,调用 performance.now() 获取页面加载开始的时间。而 init() 方法初始化检测器,启动了 DOM 变化观察、超时处理和页面加载监听等功能。

constructor() { this.startTime = performance.now(); this.init(); }
  1. 初始化 init 方法
private init(): void { this.calculateScore(); this.observer = new MutationObserver(this.handleMutations); this.observer.observe(document, { childList: true, subtree: true, attributes: true, characterData: true, }); this.timer = window.setTimeout(() => this.stop(), 10000) as unknown as number; if (document.readyState === 'complete') { this.checkFMP(); } else { window.addEventListener('load', () => this.checkFMP()); } }

calculateScore() 用于计算当前页面的得分;MutationObserver 监听 DOM 树的变化,当有新元素添加、删除或属性变化时,它会触发 handleMutations 回调,重新计算得分并检查 FMP。通过 setTimeout 设置超时机制,最多等待 10 秒钟,超时则停止检测。而 window.addEventListener('load', ...) 监听页面加载完成事件,确保在页面加载完成后调用 checkFMP() 方法检查是否已到达 FMP 时刻。

  1. 检测 FMP 时的回调 checkFMP
private checkFMP(): void { if (this.stopped || this.entries.length < 2) return; const entries = this.entries; let maxIncrease = 0; let fmpTime = 0; for (let i = 1; i < entries.length; i++) { const increase = entries[i].score - entries[i - 1].score; if (increase > maxIncrease) { maxIncrease = increase; fmpTime = entries[i].time; } } if ( maxIncrease > 0 && (document.readyState === 'complete' || maxIncrease > entries[0].score * 0.5) ) { this.stop(); if (this.onFMP) { this.onFMP(Math.round(fmpTime)); } } }

checkFMP() 通过分析每个时间点的得分,找出得分变化最显著的时刻,作为首次重要内容渲染的时刻。首先,检查 this.entries 是否至少有 2 个得分记录。然后,计算每两个时间点之间的得分变化,找出最大变化点作为潜在的 FMP 时刻。如果找到了显著的得分变化,且页面加载完成或得分变化超过 50%,则停止检测并触发 onFMP 回调,返回 FMP 时刻。

  1. DOM 变化处理 handleMutations
private handleMutations = (): void => { if (this.stopped) return; this.calculateScore(); };

每次 DOM 树发生变化时(如新增元素),会调用 handleMutations 方法。该方法会进一步调用 calculateScore(),重新计算页面的得分。

  1. 得分计算 calculateScore
private calculateScore(): void { const score = this.getPageScore(); const time = performance.now() - this.startTime; this.entries.push({ time, score: score.score }); this.checkFMP(); }

calculateScore 调用 getPageScore() 计算当前页面的得分,同时记录时间戳,并将得分和时间戳存储到 entries 数组中。在计算完得分后,调用 checkFMP() 方法检查是否已经到达 FMP 时刻。

  1. 页面得分计算 getPageScore
private getPageScore(): FMPScore { const elements: HTMLElement[] = []; let totalScore = 0; const walk = (node: HTMLElement) => { if (node.nodeType !== Node.ELEMENT_NODE) return 0; const tagName = node.tagName.toUpperCase(); if (FMPTiming.IGNORE_TAGS.has(tagName)) return 0; const rect = node.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) return 0; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; if ( rect.bottom < 0 || rect.right < 0 || rect.top > viewportHeight || rect.left > viewportWidth ) { return 0; } const weight = FMPTiming.WEIGHT_MAP[tagName as keyof typeof FMPTiming.WEIGHT_MAP] || 1; const score = rect.width * rect.height * weight; const style = window.getComputedStyle(node); const hasBgImage = style.backgroundImage && style.backgroundImage !== 'none'; const finalScore = hasBgImage ? score * 2 : score; elements.push(node); totalScore += finalScore; for (const child of Array.from(node.children)) { totalScore += walk(child as HTMLElement); } return totalScore; }; walk(document.body); return { score: totalScore, elements }; }

getPageScore 方法遍历页面的所有元素,计算每个元素的得分,并根据是否包含背景图片进行加权处理。在计算元素得分时,首先检查该元素是否可见,并计算其尺寸;若尺寸为零或超出视口,则跳过该元素。根据元素类型(如图片、视频、Canvas 等),使用不同的权重来计算得分。

参考资料

总结

主要思路是通过监控 DOM 变化来计算页面元素的得分,找出最大得分变化的时刻,从而确定 FMP。得分计算通过遍历页面中可见的元素,并根据元素类型、大小、背景图片等因素来计算得分。FMP 时刻的判定则是通过得分变化来判断,首次出现显著得分增加的时刻即为 FMP。该算法通过不断观察 DOM 更新,动态跟踪页面加载过程,并利用页面内容的变化来检测 FMP,提供了一个高效且灵活的检测机制。

最后更新于:
Copyright © 2025Moment版权所有粤ICP备2025376666