FCP - First Contentful Paint
Optimize the time to first meaningful content for better perceived performance
First Contentful Paint (FCP) measures the time from when the page starts loading to when any part of the page's content is rendered on screen. While not a Core Web Vital, it's a critical supporting metric.
Understanding FCP
Score Thresholds
| Score | Rating | User Perception |
|---|---|---|
| ≤ 1.8s | Good | Quick start |
| 1.8s - 3s | Needs Improvement | Noticeable wait |
| > 3s | Poor | Feels broken |
FCP vs LCP
| Metric | What It Measures | Example Element |
|---|---|---|
| FCP | First content visible | Loading spinner, header text |
| LCP | Largest content visible | Hero image, main heading |
A fast FCP with a slow LCP can still feel slow. Both metrics matter, but LCP is the Core Web Vital.
What Triggers FCP?
FCP is triggered by the first paint of:
- Text (any font, any size)
- Images (including background images)
- SVG content
- Non-white
<canvas>elements
// These all trigger FCP:
<h1>Hello World</h1> // Text
<img src="/logo.svg" /> // Image
<div style={{ background: 'url(bg.jpg)' }} /> // Background
<svg>...</svg> // SVGFCP Optimization Strategies
Eliminate Render-Blocking Resources
// ❌ Bad: Blocking CSS in head
<link rel="stylesheet" href="/heavy-styles.css" />
// ✅ Good: Inline critical CSS, defer rest
<style>{criticalCSS}</style>
<link
rel="stylesheet"
href="/heavy-styles.css"
media="print"
onload="this.media='all'"
/>Optimize Server Response Time
// ❌ Bad: Slow API call delays everything
export async function getServerSideProps() {
const data = await fetch('https://slow-api.com/data');
return { props: { data } };
}
// ✅ Good: Use static generation
export async function getStaticProps() {
const data = await fetch('https://slow-api.com/data');
return {
props: { data },
revalidate: 60,
};
}
// ✅ Or stream with Suspense
export default function Page() {
return (
<>
<Header /> {/* Paints immediately */}
<Suspense fallback={<Skeleton />}>
<SlowContent />
</Suspense>
</>
);
}Preload Critical Resources
export default function Layout({ children }) {
return (
<html>
<head>
{/* Preload critical font */}
<link
rel="preload"
href="/fonts/inter-var.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
{/* Preconnect to third-party domains */}
<link rel="preconnect" href="https://api.example.com" />
<link rel="dns-prefetch" href="https://cdn.example.com" />
</head>
<body>{children}</body>
</html>
);
}Optimize JavaScript Loading
module.exports = {
experimental: {
optimizePackageImports: ['lodash', 'date-fns', '@mui/material'],
},
};
// Component level
import dynamic from 'next/dynamic';
// Defer non-critical components
const Analytics = dynamic(() => import('./Analytics'), {
ssr: false,
loading: () => null,
});Use Edge Runtime
// Faster TTFB means faster FCP
export const runtime = 'edge';
export default function Page() {
return <div>Edge-rendered content</div>;
}FCP and Streaming SSR
import { Suspense } from 'react';
export default function Page() {
return (
<>
{/* Header paints immediately for fast FCP */}
<header className="h-16 bg-white border-b">
<nav>Site Navigation</nav>
</header>
{/* Main content can stream in */}
<Suspense fallback={<MainSkeleton />}>
<MainContent />
</Suspense>
{/* Footer also streams */}
<Suspense fallback={<FooterSkeleton />}>
<Footer />
</Suspense>
</>
);
}Shell Pattern for Fast FCP
export function AppShell({ children }) {
return (
<div className="min-h-screen">
{/* Static shell renders instantly */}
<header className="h-16 border-b">
<Logo />
<NavLinks />
</header>
<div className="flex">
{/* Static sidebar */}
<aside className="w-64 border-r">
<SidebarLinks />
</aside>
{/* Dynamic content streams in */}
<main className="flex-1 p-4">
{children}
</main>
</div>
</div>
);
}
// app/layout.tsx
export default function Layout({ children }) {
return (
<AppShell>
<Suspense fallback={<PageSkeleton />}>
{children}
</Suspense>
</AppShell>
);
}Measuring FCP
import { onFCP, type FCPMetric } from 'web-vitals';
export function measureFCP() {
onFCP((metric: FCPMetric) => {
console.log('FCP:', metric.value, 'ms');
// Get the FCP entry for more details
const entry = metric.entries[0];
console.log('FCP Element:', entry?.name);
sendToAnalytics({
name: 'FCP',
value: metric.value,
rating: metric.rating,
});
});
}FCP Optimization Checklist
Server Response
Use static generation where possible Pre-rendered HTML is served instantly from CDN.
Implement ISR for dynamic content Static speed with periodic updates.
Consider Edge Runtime Reduce latency by running closer to users.
Optimize database queries Slow queries delay server response.
Use CDN caching Cache responses at the edge for faster delivery.
Critical Resources
Inline critical CSS Above-the-fold styles should be in the HTML.
Preload fonts Start font download early to prevent FOUT.
Preconnect to origins Establish connections to third-party domains early.
Defer non-critical CSS Load below-fold styles after initial paint.
Minimize CSS size Smaller CSS files parse and apply faster.
JavaScript
Code split by route Each page loads only its required code.
Defer non-critical scripts Load analytics after main content renders.
Use dynamic imports Load heavy components on demand.
Tree shake dependencies Remove unused code from bundles.
Minimize bundle size Smaller bundles download and parse faster.
Real-World Impact
| Optimization | FCP Improvement |
|---|---|
| Static generation | -200ms to -800ms |
| Edge runtime | -100ms to -400ms |
| Inline critical CSS | -100ms to -300ms |
| Font preloading | -50ms to -200ms |
| Code splitting | -100ms to -300ms |