Skip to Content

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

前端性能优化Frontend Performance Metrics--FMP

Frontend Performance Metrics—FMP

FMP (First Meaningful Paint) is an important metric in Web performance optimization, used to measure the time when users see the “first meaningful content” during page loading. Specifically, it represents the first moment users see content in the browser—when both visibility and interactivity of the page change.

As a key metric for measuring page loading speed, FMP focuses on content rendering in the page’s visible area, rather than just page load completion. FMP attempts to measure the earliest moment when users can start seeing and interacting with content, not when the page is completely loaded (e.g., when the load event fires).

In other words, FMP lets you know when users can start actually perceiving and using the page during the loading process, which is crucial for optimizing user experience and interactivity.

First, let’s look at the following image:

We can see that the more “useful” content on a page typically has rich information, such as images, videos, animations, and elements that occupy large visible areas. There are two types of content that can be considered “useful”: single block elements and large elements composed of multiple elements. For example, video elements and banner images are single block elements, while image lists and multi-image combinations are element combinations.

Discussing FMP is essentially answering the question “is it useful?” The industry often considers FMP time as the first screen time. Although they’re not exactly equal in absolute accuracy, both can precisely reflect the current page’s loading and rendering performance. FMP is typically considered the moment when users obtain the main information of the page, meaning user needs are satisfied, which is why product teams often focus on the FMP metric.

To summarize, the conditions for becoming an FMP element are:

  • Large volume ratio
  • High visible screen ratio
  • Higher proportion of resource loading elements (img, svg, video, object, embed, canvas)
  • Main elements may be composed of multiple components

How to Set Up the Algorithm

After introducing the concept of FMP and its conditions, let’s look at how to design the algorithm for obtaining FMP. Based on the above introduction, we know the algorithm is divided into two parts:

  • Getting FMP elements
  • Calculating FMP elements’ loading time

The specific algorithm flow is shown in the following diagram:

Complete code is shown below:

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; // Weight configuration private static readonly WEIGHT_MAP = { IMG: 2, SVG: 2, CANVAS: 4, VIDEO: 4, OBJECT: 4, EMBED: 4, // Weight for other elements is 1 } as const; // Tags to ignore 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 { // Initial calculation this.calculateScore(); // Observe DOM changes this.observer = new MutationObserver(this.handleMutations); this.observer.observe(document, { childList: true, subtree: true, attributes: true, characterData: true, }); // Set timeout check this.timer = window.setTimeout(() => this.stop(), 10000) as unknown as number; // Listen for page load completion 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 }); // Check if this might be the FMP moment when score increases significantly 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; // Calculate element score const rect = node.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) return 0; // Check if element is in viewport const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; if ( rect.bottom < 0 || rect.right < 0 || rect.top > viewportHeight || rect.left > viewportWidth ) { return 0; } // Calculate weight const weight = FMPTiming.WEIGHT_MAP[tagName as keyof typeof FMPTiming.WEIGHT_MAP] || 1; // Calculate element score const score = rect.width * rect.height * weight; // Check background image const style = window.getComputedStyle(node); const hasBgImage = style.backgroundImage && style.backgroundImage !== 'none'; const finalScore = hasBgImage ? score * 2 : score; elements.push(node); totalScore += finalScore; // Recursively calculate child elements 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; // Find point of maximum score increase 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; } } // Stop detection if significant change found or page load complete 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;

Usage in React:

const measurePerformance = async () => { try { const fmpTime = await measureFMP(); console.log('First Meaningful Paint:', fmpTime, 'ms'); // You can send FMP data to your analytics service // await sendToAnalytics({ fmp: fmpTime }); } catch (error) { console.error('Failed to measure FMP:', error); } }; useEffect(() => { measurePerformance(); }, []);

Let’s analyze the code in detail:

  1. Constructor

In the constructor, startTime is used to record the start time by calling performance.now() to get the page load start time. The init() method initializes the detector, starting DOM change observation, timeout handling, and page load monitoring.

constructor() { this.startTime = performance.now(); this.init(); }
  1. Initialization init method
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() is used to calculate the current page’s score; MutationObserver monitors DOM tree changes, and when new elements are added, deleted, or attributes change, it triggers handleMutations callback to recalculate score and check FMP. Through setTimeout setting timeout mechanism, it waits up to 10 seconds, and if it times out, it stops detection. And window.addEventListener('load', ...) listens for page load completion events to ensure that checkFMP() method is called after page load completion to check if FMP has been reached.

  1. Detection FMP callback 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() analyzes the score of each time point to find the time point with the most significant score increase, which is considered the potential FMP moment. First, it checks if this.entries has at least 2 score records. Then, it calculates the score change between each two time points to find the maximum change point as the potential FMP moment. If a significant score change is found and the page load is complete or the score change exceeds 50%, it stops detection and triggers onFMP callback to return FMP moment.

  1. DOM change processing handleMutations
private handleMutations = (): void => { if (this.stopped) return; this.calculateScore(); };

Each time DOM tree changes (e.g., new element added), it calls handleMutations method. This method further calls calculateScore() to recalculate page score.

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

calculateScore calls getPageScore() to calculate the current page’s score, records the timestamp, and stores the score and timestamp into entries array. After calculating the score, it calls checkFMP() method to check if FMP has been reached.

  1. Page score calculation 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 method traverses all elements on the page, calculates the score of each element, and weights the score based on whether it contains a background image. When calculating the element score, it first checks if the element is visible and calculates its size; if the size is zero or outside the viewport, it skips the element. According to the element type (e.g., image, video, Canvas, etc.), different weights are used to calculate the score.

References

Summary

The main approach is to monitor DOM changes to calculate page element scores and identify the moment of maximum score change to determine FMP. Score calculation is done by traversing visible elements on the page and calculating scores based on element type, size, background images, and other factors. FMP moment is determined by score changes, with the first significant score increase being identified as FMP. The algorithm continuously observes DOM updates, dynamically tracks the page loading process, and uses page content changes to detect FMP, providing an efficient and flexible detection mechanism.

Last updated on:
Copyright © 2025Moment版权所有粤ICP备2025376666