First Meaningful Paint (FMP) was once the golden child of web performance metrics—the sophisticated metric that promised to identify the exact moment when pages became "meaningful" to users. While officially deprecated in favor of Largest Contentful Paint (LCP), understanding FMP remains valuable for grasping the evolution of performance metrics and why measuring "meaningful" content proved so challenging.
What is First Meaningful Paint?
FMP attempted to identify when a page's primary content became visible. Unlike First Contentful Paint (any content) or Largest Contentful Paint (largest content), FMP tried to detect when the most important content appeared—the content users came to see.
The algorithm worked by:
- Tracking layout changes during page load
- Identifying the "biggest layout change" after FCP
- Marking when the most significant content appeared
- Reporting this as First Meaningful Paint
The concept was appealing:
- Hero images on landing pages
- Article text on blog posts
- Product images on e-commerce sites
- Video content on media sites
However, FMP faced critical challenges:
- Inconsistent detection across different page types
- Browser variation in implementation
- Difficulty defining "meaningful" algorithmically
- Poor correlation with user perception studies
Why FMP Was Deprecated
The Measurement Problem
FMP's fundamental flaw was trying to algorithmically determine "meaningfulness":
// Simplified FMP detection logic (problematic)
function detectFMP() {
let biggestLayoutChange = 0
let fmpCandidate = 0
layoutChanges.forEach(change => {
// Problem: What defines "significant"?
const significance = calculateSignificance(change)
if (significance > biggestLayoutChange) {
biggestLayoutChange = significance
fmpCandidate = change.timestamp
}
})
return fmpCandidate
}
// Different algorithms produced different results:
// - Layout-based: Biggest visual change
// - Network-based: After main resource loads
// - Heuristic-based: Pattern matching
// All were flawed
Variability Issues
FMP measurements varied wildly:
Same Page, Different Results:
Browser A: FMP = 2.3s (detected article text)
Browser B: FMP = 1.8s (detected header image)
Tool C: FMP = 3.1s (detected after ads loaded)
Tool D: FMP = 2.7s (detected sidebar content)
This variability made FMP unreliable for:
- Performance budgets
- A/B testing
- Competitive analysis
- Optimization tracking
The LCP Solution
Largest Contentful Paint solved FMP's problems by:
- Using objective criteria (largest element)
- Providing consistent measurements across tools
- Offering clear optimization targets
- Maintaining strong correlation with user experience
FMP's Legacy: Lessons for Modern Optimization
Understanding Progressive Rendering
FMP highlighted the importance of rendering progression:
<!-- Progressive rendering strategy inspired by FMP -->
<!-- Phase 1: Critical Structure (FCP) -->
<header class="skeleton">Loading...</header>
<!-- Phase 2: Primary Content (what FMP tried to measure) -->
<main>
<article>
<h1>Main Content Title</h1>
<img src="hero.jpg" importance="high" fetchpriority="high" />
<p>Critical first paragraph...</p>
</article>
</main>
<!-- Phase 3: Secondary Content -->
<aside class="defer-load">
<!-- Loaded after primary content -->
</aside>
<!-- Phase 4: Enhancement (after TTI) -->
<div id="comments" data-load="lazy">
<!-- Loaded on scroll or idle -->
</div>
The Hero Element Pattern
FMP's focus on "meaningful" content established the hero element pattern:
// Modern hero element tracking (replacing FMP)
class HeroElementTimer {
constructor() {
this.heroElements = new Map()
}
markHeroElement(selector, label) {
const element = document.querySelector(selector)
if (!element) return
// Use Element Timing API
element.setAttribute("elementtiming", label)
// Or use Intersection Observer
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const timing = performance.now()
this.heroElements.set(label, timing)
console.log(`Hero element "${label}" visible at ${timing}ms`)
observer.disconnect()
}
})
})
observer.observe(element)
}
// Track multiple hero elements
init() {
this.markHeroElement(".hero-image", "hero-image")
this.markHeroElement("main h1", "main-heading")
this.markHeroElement(".product-image", "product-image")
this.markHeroElement(".video-player", "video")
}
// Get synthetic "FMP" based on hero elements
getSyntheticFMP() {
const timings = Array.from(this.heroElements.values())
return timings.length > 0 ? Math.max(...timings) : null
}
}
Content Priority Strategies
FMP taught us to prioritize content deliberately:
/* Content priority hints inspired by FMP */
/* Priority 1: Hero content (what FMP aimed to measure) */
.hero-content {
/* Ensure fast rendering */
content-visibility: visible;
contain: layout;
}
.hero-image {
/* Prioritize loading */
content: url("hero.jpg");
loading: eager;
fetchpriority: high;
}
/* Priority 2: Primary content */
.main-content {
/* Render after hero */
content-visibility: auto;
contain-intrinsic-size: 0 500px;
}
/* Priority 3: Secondary content */
.secondary-content {
/* Defer rendering */
content-visibility: auto;
contain-intrinsic-size: 0 300px;
}
/* Priority 4: Below-fold content */
.below-fold {
/* Skip rendering initially */
content-visibility: auto;
contain-intrinsic-size: 0 1000px;
}
Migrating from FMP to LCP
Measurement Transition
If you were tracking FMP, transition to LCP:
// Old: FMP measurement (deprecated)
const paintObserver = new PerformanceObserver(list => {
list.getEntries().forEach(entry => {
if (entry.name === "first-meaningful-paint") {
console.log("FMP:", entry.startTime) // No longer works
}
})
})
// New: LCP measurement
const lcpObserver = new PerformanceObserver(list => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
console.log("LCP:", lastEntry.startTime)
console.log("LCP element:", lastEntry.element)
})
lcpObserver.observe({ entryTypes: ["largest-contentful-paint"] })
// Also track custom hero elements
const heroObserver = new PerformanceObserver(list => {
list.getEntries().forEach(entry => {
console.log(`Hero element "${entry.identifier}": ${entry.startTime}ms`)
})
})
heroObserver.observe({ entryTypes: ["element"] })
Optimization Strategy Update
Shift focus from "meaningful" to "largest":
<!-- Old: Optimize for FMP (ambiguous target) -->
<main>
<!-- What's "meaningful"? Unclear -->
<h1>Page Title</h1>
<img src="image.jpg" />
<p>Content...</p>
</main>
<!-- New: Optimize for LCP (clear target) -->
<main>
<!-- Largest element is clear optimization target -->
<img
src="hero.jpg"
width="1200"
height="600"
fetchpriority="high"
alt="Hero"
class="lcp-element"
/>
<h1>Page Title</h1>
<p>Content...</p>
</main>
Performance Budget Updates
Update your performance budgets:
// Old performance budget (FMP-based)
const oldBudget = {
"first-contentful-paint": 2000,
"first-meaningful-paint": 2500, // Deprecated
"time-to-interactive": 5000
}
// New performance budget (LCP-based)
const newBudget = {
"first-contentful-paint": 1800,
"largest-contentful-paint": 2500, // Replaced FMP
"total-blocking-time": 200,
"cumulative-layout-shift": 0.1,
"time-to-interactive": 3800
}
// Lighthouse CI configuration
module.exports = {
ci: {
assert: {
assertions: {
"first-contentful-paint": ["error", { maxNumericValue: 1800 }],
"largest-contentful-paint": ["error", { maxNumericValue: 2500 }],
"total-blocking-time": ["error", { maxNumericValue: 200 }],
"cumulative-layout-shift": ["error", { maxNumericValue: 0.1 }]
}
}
}
}
What FMP Got Right
Despite deprecation, FMP introduced valuable concepts:
1. User-Centric Performance
FMP pioneered thinking about what users actually care about:
// FMP-inspired user-centric metrics
class UserCentricMetrics {
constructor() {
this.metrics = {}
}
// Track when specific user goals are achievable
trackGoalAchievable(goalName, callback) {
const startTime = performance.now()
const checkGoal = () => {
if (callback()) {
this.metrics[goalName] = performance.now() - startTime
console.log(
`Goal "${goalName}" achievable at ${this.metrics[goalName]}ms`
)
} else {
requestAnimationFrame(checkGoal)
}
}
checkGoal()
}
// Example: E-commerce site
init() {
// When can users see products?
this.trackGoalAchievable("products-visible", () => {
return document.querySelectorAll(".product").length > 0
})
// When can users interact with cart?
this.trackGoalAchievable("cart-ready", () => {
const cartButton = document.querySelector(".add-to-cart")
return cartButton && !cartButton.disabled
})
// When is search functional?
this.trackGoalAchievable("search-ready", () => {
const searchInput = document.querySelector("#search")
return searchInput && searchInput.dataset.ready === "true"
})
}
}
2. Layout Stability Awareness
FMP's layout tracking presaged CLS concerns:
// FMP-inspired layout monitoring
class LayoutStabilityMonitor {
constructor() {
this.layoutShifts = []
this.significantChanges = []
}
init() {
// Track all layout shifts
new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
this.layoutShifts.push({
value: entry.value,
time: entry.startTime,
sources: entry.sources
})
// Identify significant changes (FMP-like)
if (entry.value > 0.1) {
this.significantChanges.push(entry)
}
}
}).observe({ entryTypes: ["layout-shift"] })
}
// Get "meaningful" layout completion time
getMeaningfulLayoutTime() {
if (this.significantChanges.length === 0) return null
// Last significant change (FMP-inspired)
return this.significantChanges[this.significantChanges.length - 1].startTime
}
}
3. Progressive Enhancement Focus
FMP encouraged progressive content revelation:
// Progressive rendering strategy
class ProgressiveRenderer {
constructor() {
this.phases = []
}
addPhase(phase) {
this.phases.push({
...phase,
startTime: null,
endTime: null
})
}
async render() {
for (const phase of this.phases) {
phase.startTime = performance.now()
// Render phase content
await this.renderPhase(phase)
phase.endTime = performance.now()
// Log "meaningful" paint moments
console.log(
`Phase "${phase.name}" rendered in ${phase.endTime - phase.startTime}ms`
)
// Yield to browser
await this.yieldToMain()
}
}
async renderPhase(phase) {
if (phase.critical) {
// Render immediately (FCP)
phase.render()
} else if (phase.meaningful) {
// What FMP tried to capture
await this.waitForIdle()
phase.render()
} else {
// Defer non-meaningful content
requestIdleCallback(() => phase.render())
}
}
yieldToMain() {
return new Promise(resolve => setTimeout(resolve, 0))
}
waitForIdle() {
return new Promise(resolve => {
if ("requestIdleCallback" in window) {
requestIdleCallback(resolve)
} else {
setTimeout(resolve, 100)
}
})
}
}
// Usage
const renderer = new ProgressiveRenderer()
renderer.addPhase({
name: "shell",
critical: true,
render: () => renderAppShell()
})
renderer.addPhase({
name: "hero",
meaningful: true, // FMP-inspired
render: () => renderHeroContent()
})
renderer.addPhase({
name: "secondary",
render: () => renderSecondaryContent()
})
renderer.render()
Modern Alternatives to FMP
Element Timing API
Track specific elements instead of guessing "meaningful":
<!-- Mark elements for timing -->
<img elementtiming="hero-image" src="hero.jpg" />
<h1 elementtiming="main-heading">Welcome</h1>
<div elementtiming="key-content">Important content...</div>
<script>
// Observe element timing
new PerformanceObserver(list => {
list.getEntries().forEach(entry => {
console.log(`Element "${entry.identifier}": ${entry.startTime}ms`)
// Send to analytics
analytics.track("element_timing", {
element: entry.identifier,
renderTime: entry.startTime,
loadTime: entry.loadTime,
size: entry.intersectionRect.width * entry.intersectionRect.height
})
})
}).observe({ entryTypes: ["element"] })
</script>
Custom Metrics
Create business-specific "meaningful" metrics:
// E-commerce: Time to first product visible
function timeToFirstProduct() {
return new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
const visibleProduct = entries.find(e => e.isIntersecting)
if (visibleProduct) {
resolve(performance.now())
observer.disconnect()
}
})
// Observe all products
document.querySelectorAll(".product").forEach(product => {
observer.observe(product)
})
})
}
// News site: Time to article text
function timeToArticleText() {
return new Promise(resolve => {
const checkArticle = () => {
const article = document.querySelector("article")
const hasText = article && article.textContent.length > 100
if (hasText) {
resolve(performance.now())
} else {
requestAnimationFrame(checkArticle)
}
}
checkArticle()
})
}
// SaaS: Time to interactive dashboard
function timeToDashboardReady() {
return new Promise(resolve => {
const checkDashboard = () => {
const charts = document.querySelectorAll('.chart[data-loaded="true"]')
const minCharts = 3 // Business requirement
if (charts.length >= minCharts) {
resolve(performance.now())
} else {
requestAnimationFrame(checkDashboard)
}
}
checkDashboard()
})
}
Frequently Asked Questions
Can I still measure FMP?
FMP is no longer supported in modern browsers or tools. Lighthouse removed FMP in version 6.0 (2020). Use LCP instead, or implement custom metrics for specific "meaningful" content using the Element Timing API.
How do I convert FMP targets to LCP?
Generally, LCP targets should be similar or slightly higher than FMP targets:
- FMP < 2s → LCP < 2.5s
- FMP < 3s → LCP < 3.5s
- FMP < 4s → LCP < 4.5s
However, measure actual LCP values rather than assuming conversion ratios.
Why did LCP succeed where FMP failed?
LCP succeeds because it:
- Uses objective criteria (largest element)
- Provides consistent measurements
- Correlates well with user experience
- Offers clear optimization targets
- Works reliably across all tools
FMP failed due to subjective "meaningful" determination.
Should I still optimize for "meaningful" content?
Absolutely! While FMP is deprecated, the concept remains valid:
- Prioritize primary content
- Use Element Timing for specific elements
- Create custom business metrics
- Focus on user goals
The difference is measuring specific elements rather than relying on algorithms.
What happened to FMP in my historical data?
Historical FMP data remains valid for trend analysis within the same tool, but shouldn't be compared across tools or used for future targets. Transition to LCP for ongoing monitoring and establish new baselines.