View contents
Complete FCP Optimization Guide: Achieving Fast First Contentful Paint
Introduction
First Contentful Paint (FCP) measures when users first see content on your page. Achieving a good FCP score (under 1.8 seconds) requires optimizing the critical rendering path—the sequence of steps browsers take to convert HTML, CSS, and JavaScript into visible pixels.
This guide covers advanced strategies to eliminate delays and get content to users as quickly as possible.
Understanding the Critical Rendering Path
Before FCP can occur, the browser must:
Critical Rendering Path:
1. Download HTML
2. Parse HTML, discover resources
3. Download critical CSS
4. Parse CSS, build CSSOM
5. Build render tree (DOM + CSSOM)
6. Calculate layout
7. Paint content → FCP!
Key insight: FCP is blocked by:
- HTML download and parsing
- CSS download and parsing
- Synchronous JavaScript
Optimizing Server Response Time
1. Improve TTFB (Time to First Byte)
Every millisecond of TTFB delays FCP:
TTFB Components:
├── DNS Lookup: 20-120ms
├── TCP Connection: 20-100ms
├── TLS Handshake: 30-100ms
├── Server Processing: Variable
└── Network Latency: Variable
Optimization targets:
├── Total TTFB < 200ms (excellent)
├── Total TTFB < 500ms (good)
└── Total TTFB > 800ms (needs work)
2. Use a CDN
Serve content from edge servers near users:
Without CDN:
User (Chile) → Origin Server (US East) = 180ms latency
With CDN:
User (Chile) → CDN Edge (Santiago) = 20ms latency
3. Implement Caching
# Nginx - Cache static resources
location ~* \.(css|js|jpg|jpeg|png|webp|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Cache HTML with revalidation
location / {
add_header Cache-Control "public, max-age=0, must-revalidate";
}
4. Enable Compression
# Enable Brotli (preferred) and Gzip compression
gzip on;
gzip_types text/plain text/css application/javascript application/json;
gzip_min_length 256;
# Brotli (if available)
brotli on;
brotli_types text/plain text/css application/javascript application/json;
Eliminating Render-Blocking CSS
1. Identify Critical CSS
Critical CSS = styles needed for above-the-fold content:
<!-- Critical CSS inlined in <head> -->
<style>
/* Only styles for initial viewport */
:root { --primary: #071952; --text: #333; }
body { margin: 0; font-family: system-ui; color: var(--text); }
.header { background: var(--primary); padding: 1rem; }
.hero { padding: 2rem; }
.hero h1 { font-size: 2.5rem; margin: 0; }
</style>
2. Extract and Inline Critical CSS
Use tools to automatically extract critical CSS:
# Using critical npm package
npx critical index.html --inline --minify
# Using PurgeCSS for unused CSS removal
npx purgecss --css styles.css --content index.html
3. Load Non-Critical CSS Asynchronously
<!-- Pattern 1: preload + onload -->
<link rel="preload" href="full-styles.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript>
<link rel="stylesheet" href="full-styles.css">
</noscript>
<!-- Pattern 2: media query swap -->
<link rel="stylesheet" href="full-styles.css"
media="print" onload="this.media='all'">
<!-- Pattern 3: Dynamic injection -->
<script>
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'full-styles.css';
document.head.appendChild(link);
</script>
4. Minimize CSS
Keep CSS small and efficient:
/* ❌ Verbose CSS */
.button {
background-color: #071952;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 20px;
padding-right: 20px;
border-radius: 4px;
}
/* ✅ Minified CSS */
.button{background:#071952;padding:10px 20px;border-radius:4px}
Eliminating Render-Blocking JavaScript
1. Defer Non-Critical Scripts
<!-- ❌ Blocks rendering -->
<script src="analytics.js"></script>
<!-- ✅ Deferred - runs after DOM ready -->
<script src="analytics.js" defer></script>
<!-- ✅ Async - runs when downloaded, order not guaranteed -->
<script src="widget.js" async></script>
2. Move Scripts to End of Body
<!DOCTYPE html>
<html>
<head>
<style>/* Critical CSS only */</style>
</head>
<body>
<!-- Content that triggers FCP -->
<header>...</header>
<main>...</main>
<!-- Scripts at end -->
<script src="app.js"></script>
</body>
</html>
3. Use Module Scripts
<!-- Module scripts are deferred by default -->
<script type="module" src="app.js"></script>
<!-- Preload critical modules -->
<link rel="modulepreload" href="critical-module.js">
Optimizing Resource Loading
1. Preload Critical Resources
Tell browser what’s important:
<head>
<!-- Preload critical CSS if not inlined -->
<link rel="preload" href="critical.css" as="style">
<!-- Preload hero image -->
<link rel="preload" href="hero.webp" as="image"
fetchpriority="high">
<!-- Preload critical font -->
<link rel="preload" href="font.woff2" as="font"
type="font/woff2" crossorigin>
</head>
2. Use Resource Hints
<!-- DNS prefetch for third-party domains -->
<link rel="dns-prefetch" href="https://cdn.example.com">
<!-- Preconnect for critical third parties -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Prefetch resources for next navigation -->
<link rel="prefetch" href="next-page.html">
3. Prioritize Visible Content
Use fetchpriority attribute:
<!-- High priority for LCP image -->
<img src="hero.webp" fetchpriority="high" alt="Hero">
<!-- Low priority for below-fold images -->
<img src="footer-logo.webp" fetchpriority="low" alt="Logo">
Font Optimization for FCP
1. Use font-display
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap; /* Show fallback immediately, swap when ready */
}
2. Preload Critical Fonts
<link rel="preload" href="primary-font.woff2"
as="font" type="font/woff2" crossorigin>
3. Use System Font Stack as Fallback
body {
font-family: 'CustomFont', -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
4. Subset Fonts
Remove unused characters:
# Using glyphanger
glyphanger --subset="*.woff2" --whitelist="U+0-7F"
Server-Side Rendering Benefits
SSR can dramatically improve FCP:
Client-Side Rendering (CSR):
HTML Shell → JS Download → JS Execute → Content Render → FCP
└────────────── Long delay ──────────────┘
Server-Side Rendering (SSR):
Full HTML → FCP! (immediate) → JS Download → Hydration
↑ FCP happens much faster
Static Site Generation (SSG)
For content that doesn’t change often:
SSG Process:
Build Time: Generate HTML for all pages
Runtime: Serve pre-built HTML → Immediate FCP
Measuring and Monitoring FCP
Using Performance API
// Get FCP value
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
console.log('FCP:', entry.startTime, 'ms');
}
}
});
observer.observe({ type: 'paint', buffered: true });
Using Web Vitals Library
import { onFCP } from 'web-vitals';
onFCP((metric) => {
console.log('FCP:', metric.value);
// Send to analytics
analytics.track('FCP', {
value: metric.value,
rating: metric.rating, // 'good', 'needs-improvement', 'poor'
url: window.location.href
});
});
FCP Optimization Checklist
Server Response
├── [ ] TTFB under 200ms
├── [ ] CDN configured
├── [ ] Compression enabled (Brotli/Gzip)
├── [ ] HTTP/2 or HTTP/3 enabled
└── [ ] Proper caching headers
CSS Optimization
├── [ ] Critical CSS inlined
├── [ ] Non-critical CSS loaded async
├── [ ] CSS minified
├── [ ] Unused CSS removed
└── [ ] No @import statements
JavaScript Optimization
├── [ ] Scripts deferred or async
├── [ ] No render-blocking JS in head
├── [ ] Critical JS inlined if small
└── [ ] Third-party scripts deferred
Resource Loading
├── [ ] Critical resources preloaded
├── [ ] Resource hints configured
├── [ ] Images optimized
└── [ ] Fonts preloaded with font-display
Monitoring
├── [ ] FCP tracked in RUM
├── [ ] Alerts for FCP regression
├── [ ] Regular Lighthouse audits
└── [ ] Field data reviewed in CrUX
Common FCP Problems and Solutions
| Problem | Solution |
|---|---|
| Large CSS file blocks render | Extract and inline critical CSS |
| Synchronous JS in head | Add defer or move to body end |
| Slow server response | Use CDN, enable caching |
| Large HTML document | Reduce DOM size, use SSR |
| Slow third-party resources | Defer, use facades, or remove |
| Web fonts delay text | Use font-display: swap, preload |
Related Articles
- FCP Explained - FCP fundamentals
- LCP Optimization Guide - Optimize largest content
- TTFB Optimization Guide - Server response optimization
- Critical CSS Guide - Extract and inline critical styles
References
This article was enhanced using authoritative sources:
[1] web.dev: Optimize First Contentful Paint https://web.dev/articles/optimize-fcp Google’s official guide to FCP optimization. Covers the critical rendering path, render-blocking resources, and specific techniques for improving FCP including CSS delivery and JavaScript loading strategies.
[2] Chrome Developers: Critical Rendering Path https://developer.chrome.com/docs/lighthouse/performance/critical-request-chains Documentation on critical request chains and how to optimize resource loading order. Explains how request chains affect FCP and how to minimize chain depth.
[3] MDN Web Docs: CSS Performance Optimization https://developer.mozilla.org/en-US/docs/Learn/Performance/CSS Technical reference for CSS optimization including critical CSS extraction, selector efficiency, and async loading patterns that directly impact FCP.
[4] web.dev: Eliminate Render-Blocking Resources https://web.dev/articles/render-blocking-resources Comprehensive guide to identifying and removing render-blocking CSS and JavaScript. Covers inline critical CSS, defer/async attributes, and resource prioritization.
Note: This article is part of our SEO analysis series. Explore all articles in the Performance SEO Hub.
Sources: web.dev (FCP Optimization), Chrome Developers, MDN Web Docs