Technical

First Meaningful Paint (FMP)

A deprecated performance metric that measured when the primary content of a page became visible to users, now replaced by Largest Contentful Paint (LCP).

Quick Answer

  • What it is: A deprecated performance metric that measured when the primary content of a page became visible to users, now replaced by Largest Contentful Paint (LCP).
  • Why it matters: FMP attempted to identify when users perceive the page as useful, but was replaced by LCP due to inconsistent measurements.
  • How to check or improve: While deprecated, understanding FMP helps optimize for LCP by focusing on rendering primary content quickly.

When you'd use this

FMP attempted to identify when users perceive the page as useful, but was replaced by LCP due to inconsistent measurements.

Example scenario

Hypothetical scenario (not a real company)

A team might use First Meaningful Paint (FMP) when While deprecated, understanding FMP helps optimize for LCP by focusing on rendering primary content quickly.

Common mistakes

  • Confusing First Meaningful Paint (FMP) with Largest Contentful Paint (LCP): A Core Web Vitals metric that measures the render time of the largest image or text block visible within the viewport, relative to when the page first started loading.
  • Confusing First Meaningful Paint (FMP) with First Contentful Paint (FCP): A performance metric that measures the time from when a page starts loading to when any part of the page's content is rendered on the screen.
  • Confusing First Meaningful Paint (FMP) with Speed Index: A performance metric that measures how quickly the contents of a page are visibly populated, calculated as the average time at which visible parts of the page are displayed.

How to measure or implement

  • While deprecated, understanding FMP helps optimize for LCP by focusing on rendering primary content quickly

Analyze your LCP performance instead

Start here
Updated Jan 20, 2026·4 min read

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:

  1. Tracking layout changes during page load
  2. Identifying the "biggest layout change" after FCP
  3. Marking when the most significant content appeared
  4. 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.

Put GEO into practice

Generate AI-optimized content that gets cited.

Try Rankwise Free
Newsletter

Stay ahead of AI search

Weekly insights on GEO and content optimization.