First Contentful Paint (FCP) is the performance metric that captures the moment users first see visual confirmation that your page is loading. Unlike TTFB which measures server response, or LCP which waits for the largest element, FCP marks when any content first appears—whether it's text, an image, or even a loading spinner. This makes it a critical metric for perceived performance.
What is First Contentful Paint?
FCP measures the time from navigation start (when the user clicks a link or enters a URL) to when the browser renders the first bit of content from the DOM. This content can be:
- Text (with any font, including web fonts)
- Images (including background images)
- SVG elements
- Non-white canvas elements
- Video elements (poster frame)
FCP specifically excludes:
- iframes (content inside iframes doesn't count)
- White or transparent backgrounds
- Content hidden with CSS
Google's FCP thresholds are:
- Good: 1.8 seconds or less
- Needs Improvement: 1.8 to 3.0 seconds
- Poor: Over 3.0 seconds
These thresholds apply to the 75th percentile of page loads, segmented across mobile and desktop devices.
Why FCP Matters for User Experience
The Psychology of First Paint
FCP addresses a fundamental aspect of human psychology: the need for feedback. When users click a link, they need immediate confirmation that something is happening. Without this feedback:
- Users assume the site is broken
- They click multiple times (rage clicking)
- They hit the back button
- They develop negative brand associations
Research shows:
- 53% of users abandon sites that take over 3 seconds to show content
- 1-second delay in FCP reduces conversions by 7%
- 100ms improvement in FCP increases engagement by 1.5%
FCP's Role in the Performance Perception
FCP is part of the progressive rendering experience:
- FCP (First Contentful Paint): Something appears
- FMP (First Meaningful Paint): Primary content visible
- LCP (Largest Contentful Paint): Main content rendered
- TTI (Time to Interactive): Page fully interactive
Users perceive sites with fast FCP as significantly faster overall, even if total load time is similar. This "perceived performance" often matters more than actual load time.
Business Impact
Companies optimizing FCP report significant improvements:
Pinterest reduced FCP by 40%:
- 15% increase in SEO traffic
- 15% increase in signup conversion
Walmart improved FCP by 1 second:
- 2% increase in conversions
- 1% increase in revenue per visitor
BBC found that each additional second to FCP:
- Lost 10% of users
- Reduced page views by 4.5%
How FCP Works: The Rendering Pipeline
The Critical Rendering Path
FCP depends on the critical rendering path—the sequence of steps browsers must complete before rendering:
Navigation Start
↓
DNS + TCP + TLS
↓
HTTP Request/Response (TTFB)
↓
Parse HTML
↓
Parse CSS → Build CSSOM
↓
Parse JavaScript (if render-blocking)
↓
Build Render Tree
↓
Layout/Reflow
↓
Paint ← FCP MEASURED HERE
Render-Blocking Resources
Resources that delay FCP:
CSS:
- All CSS is render-blocking by default
- Browser waits for CSSOM construction
- Even non-critical CSS blocks first paint
JavaScript:
- Synchronous scripts block parsing
- Scripts without
asyncordeferdelay rendering - Inline scripts can block if placed incorrectly
Fonts:
- Can cause Flash of Invisible Text (FOIT)
- Delays text rendering until fonts load
- Without
font-display, text remains invisible
Browser Optimizations
Modern browsers implement optimizations affecting FCP:
Speculative Parsing: Browsers pre-scan for resources while blocked:
<!-- Browser discovers and fetches image while parsing CSS -->
<link rel="stylesheet" href="styles.css" />
<img src="hero.jpg" />
Early Flush: Servers can send partial HTML:
// Node.js early flush example
app.get('/', (req, res) => {
res.write(`
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/critical.css">
</head>
<body>
<header>Loading...</header>
`);
// Flush early content
res.flush();
// Process and send rest
const content = await generateContent();
res.end(content);
});
Best Practices for Optimizing FCP
1. Optimize the Critical Rendering Path
Inline Critical CSS
<style>
/* Critical above-the-fold styles */
body {
margin: 0;
font-family: system-ui;
}
.header {
background: #333;
color: white;
padding: 1rem;
}
.hero {
min-height: 400px;
background: #f0f0f0;
}
</style>
<!-- Load non-critical CSS asynchronously -->
<link
rel="preload"
href="/css/main.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript><link rel="stylesheet" href="/css/main.css" /></noscript>
Extract Critical CSS Automatically
// Using critical npm package
const critical = require("critical")
critical.generate({
inline: true,
base: "dist/",
src: "index.html",
target: "index-critical.html",
width: 1300,
height: 900,
extract: true, // Extract and inline critical CSS
ignore: {
atrule: ["@font-face"]
}
})
2. Eliminate Render-Blocking JavaScript
Use Async and Defer
<!-- Bad: Render-blocking -->
<script src="app.js"></script>
<!-- Good: Deferred execution -->
<script defer src="app.js"></script>
<!-- Good: Async for independent scripts -->
<script async src="analytics.js"></script>
<!-- Best: Module scripts are deferred by default -->
<script type="module" src="app.js"></script>
Load JavaScript Conditionally
<script>
// Load non-critical features after FCP
if ("requestIdleCallback" in window) {
requestIdleCallback(() => {
import("./features.js")
})
} else {
setTimeout(() => import("./features.js"), 1)
}
</script>
3. Optimize Font Loading
Use font-display
@font-face {
font-family: "Brand Font";
src: url("/fonts/brand.woff2") format("woff2");
font-display: swap; /* Show fallback immediately */
/* Other options:
* block - Hide text up to 3s (FOIT)
* swap - Show fallback immediately (FOUT)
* fallback - Hide briefly, then fallback
* optional - Use if cached, else fallback
*/
}
Preload Key Fonts
<!-- Preload critical fonts -->
<link
rel="preload"
href="/fonts/main.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<!-- Preconnect to font providers -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
4. Implement Server-Side Rendering (SSR)
Next.js SSR Example
// pages/index.js
export async function getServerSideProps() {
const data = await fetchData()
return {
props: { data } // Pre-rendered with data
}
}
export default function Home({ data }) {
return (
<div>
<h1>{data.title}</h1>
<p>{data.content}</p>
</div>
)
}
Static Generation for Better FCP
// Even better: Static generation
export async function getStaticProps() {
const data = await fetchData()
return {
props: { data },
revalidate: 3600 // Regenerate every hour
}
}
5. Optimize Network Performance
Enable Compression
# Nginx gzip configuration
gzip on;
gzip_types text/plain text/css text/javascript
application/javascript application/json
text/xml application/xml image/svg+xml;
gzip_min_length 1000;
gzip_comp_level 6;
# Brotli compression (better than gzip)
brotli on;
brotli_types text/plain text/css text/javascript
application/javascript application/json;
brotli_comp_level 6;
HTTP/2 Push Critical Resources
// Express with HTTP/2 push
app.get("/", (req, res) => {
// Push critical CSS
if (res.push) {
const cssStream = res.push("/css/critical.css", {
request: { accept: "text/css" },
response: { "content-type": "text/css" }
})
cssStream.end(criticalCSS)
}
res.send(html)
})
Common FCP Issues and Solutions
Issue 1: Web Font Loading Delays
Problem: Text remains invisible until fonts load
Solutions:
/* Use system fonts for critical text */
.hero-title {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* Load brand font for enhancement */
.hero-title.font-loaded {
font-family: "Brand Font", sans-serif;
}
// Font loading API
if ("fonts" in document) {
document.fonts.ready.then(() => {
document.body.classList.add("fonts-loaded")
})
}
Issue 2: Large CSS Bundles
Problem: Waiting for entire CSS file before rendering
Solution: Split and prioritize CSS
<!-- Critical inline CSS -->
<style>
/* Minified critical CSS here */
</style>
<!-- Preload main CSS -->
<link rel="preload" href="/css/main.css" as="style" />
<!-- Load non-critical CSS -->
<link
rel="stylesheet"
href="/css/main.css"
media="print"
onload="this.media='all'; this.onload=null;"
/>
Issue 3: Third-Party Scripts
Problem: Analytics and ads blocking render
Solution: Defer third-party scripts
<!-- Load after window.load -->
<script>
window.addEventListener("load", () => {
// Load analytics
const ga = document.createElement("script")
ga.src = "https://www.google-analytics.com/analytics.js"
ga.async = true
document.head.appendChild(ga)
// Load other third-party scripts
setTimeout(() => {
loadChatWidget()
loadSocialWidgets()
}, 2000)
})
</script>
Issue 4: Slow Server Response
Problem: High TTFB delaying FCP
Solution: Implement caching and CDN
// Service worker for instant FCP on repeat visits
self.addEventListener("fetch", event => {
event.respondWith(
caches.match(event.request).then(response => {
// Cache hit - return response immediately
if (response) {
return response
}
// Cache miss - fetch and cache
return fetch(event.request).then(response => {
const responseClone = response.clone()
caches.open("v1").then(cache => {
cache.put(event.request, responseClone)
})
return response
})
})
)
})
Issue 5: JavaScript-Dependent Content
Problem: Nothing renders until JavaScript executes
Solution: Progressive enhancement
<!-- Base HTML that renders immediately -->
<div class="product-list">
<div class="product-skeleton">Loading products...</div>
</div>
<!-- Enhance with JavaScript -->
<script type="module">
import { renderProducts } from "./products.js"
renderProducts()
</script>
<!-- NoScript fallback -->
<noscript>
<div class="product-list">
<!-- Server-rendered product list -->
<?php include 'products-static.php'; ?>
</div>
</noscript>
Tools for Measuring FCP
Browser DevTools
Chrome DevTools Performance Panel:
// Programmatically measure FCP
new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.name === "first-contentful-paint") {
console.log(`FCP: ${entry.startTime}ms`)
// Send to analytics
analytics.track("FCP", {
value: entry.startTime,
url: window.location.href
})
}
}
}).observe({ entryTypes: ["paint"] })
Lighthouse
# Command line Lighthouse
lighthouse https://example.com --only-categories=performance
# Programmatic usage
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
async function measureFCP(url) {
const chrome = await chromeLauncher.launch({chromeFlags: ['--headless']});
const result = await lighthouse(url, {
port: chrome.port,
onlyCategories: ['performance']
});
console.log(`FCP: ${result.lhr.audits['first-contentful-paint'].displayValue}`);
await chrome.kill();
}
Web Vitals Library
import { getFCP } from "web-vitals"
getFCP(metric => {
console.log(`FCP: ${metric.value}ms`)
console.log(`Rating: ${metric.rating}`) // 'good', 'needs-improvement', or 'poor'
// Get detailed attribution
const navEntry = performance.getEntriesByType("navigation")[0]
console.log(`DNS: ${navEntry.domainLookupEnd - navEntry.domainLookupStart}ms`)
console.log(`TCP: ${navEntry.connectEnd - navEntry.connectStart}ms`)
console.log(`Request: ${navEntry.responseStart - navEntry.requestStart}ms`)
})
FCP Optimization by Platform
WordPress
// Inline critical CSS in WordPress
function inline_critical_css() {
$critical_css = file_get_contents(get_template_directory() . '/critical.css');
echo "<style>{$critical_css}</style>";
}
add_action('wp_head', 'inline_critical_css', 1);
// Defer non-critical scripts
function defer_scripts($tag, $handle) {
$defer_scripts = ['jquery', 'main-script'];
if (in_array($handle, $defer_scripts)) {
return str_replace(' src=', ' defer src=', $tag);
}
return $tag;
}
add_filter('script_loader_tag', 'defer_scripts', 10, 2);
React Applications
// Code splitting for faster FCP
import { lazy, Suspense } from "react"
// Load immediately for FCP
const Header = () => <header>My App</header>
const LoadingSpinner = () => <div>Loading...</div>
// Lazy load heavy components
const Dashboard = lazy(() => import("./Dashboard"))
const Analytics = lazy(() => import("./Analytics"))
function App() {
return (
<div>
<Header /> {/* Renders immediately for FCP */}
<Suspense fallback={<LoadingSpinner />}>
<Dashboard />
<Analytics />
</Suspense>
</div>
)
}
Frequently Asked Questions
What's the difference between FCP and LCP?
FCP measures when any content first appears (even a loading spinner), while LCP measures when the largest content element appears (usually the main content). FCP happens first and indicates the page is loading; LCP indicates the main content is ready.
Why is my FCP slower on mobile?
Mobile devices typically have:
- Slower CPUs (3-5x slower JavaScript execution)
- Higher network latency (especially on cellular)
- Less memory for caching
- Throttled performance on battery saver mode
Optimize specifically for mobile constraints.
Can a fast FCP hide a slow overall load?
Yes, this is called "progressive rendering." A fast FCP with skeleton screens or loading states improves perceived performance even if total load time is longer. However, don't sacrifice LCP for FCP—balance both metrics.
How do Single Page Applications affect FCP?
SPAs often have poor FCP because they must download, parse, and execute JavaScript before rendering anything. Use SSR, static generation, or progressive enhancement to improve SPA FCP.
Should I prioritize FCP or LCP?
Both are important, but LCP is a Core Web Vital (direct ranking factor) while FCP isn't. However, FCP strongly influences user perception and bounce rate. Optimize for both, but if forced to choose, prioritize LCP for SEO.