First Contentful Paint (FCP): Stop the blank screen that kills trust
User clicks your link. White screen. One second. Still white. Two seconds. They're questioning whether they clicked the right thing. Three seconds. They hit back. You never got a chance to show them anything.

What is FCP (and why blank screens kill conversions)?
FCP measures when the browser paints the first pixel of content. Any text, image, or background colour that's not white. Before FCP, your page is indistinguishable from being broken.
Users make snap judgements: If they see nothing for 2 seconds, they assume the page didn't load, their internet is broken, or they clicked the wrong link. They leave. Even if your full LCP is only 2.5 seconds, that blank screen at the start erodes trust.
FCP thresholds
FCP vs LCP: FCP measures when anything renders. LCP measures when the main content renders. You want both to be fast, but LCP is the Core Web Vital.
What content counts for FCP?
Counts for FCP:
✓ Text (even if web font hasn't loaded)
✓ Images (including background images)
✓ SVG elements
✓ Canvas elements
✓ Non-white background colors on elements
Does NOT count:
✗ Iframes
✗ White canvas/SVG (no visible content)
✗ Loading spinners (usually)Why FCP matters for user experience
FCP is about perceived speed. Users decide whether to wait or bounce within the first 1-2 seconds. Showing something early keeps them engaged while the rest loads.
First impressions
A blank screen for 3 seconds feels like forever. Users assume the page is broken and leave. Fast FCP signals "this site works."
Bounce rate impact
Every second of delay in FCP increases bounce rates by 8-12%. Mobile users on slow networks are especially sensitive to blank screen time.
Mobile performance
Mobile devices have slower CPUs. Parse and render time matters more. Optimising FCP disproportionately helps mobile users.
Research shows that improving FCP from 3s to 1.5s can reduce bounce rates by 15-20%. Users are patient if they see progress.
How to measure FCP
Field data (Real users)
FCP varies by device, network, and location. Field data shows real user experience:
- Iron/Out free benchmark - Get your FCP from Chrome UX Report
- PageSpeed Insights - Shows field FCP data from Chrome UX Report
- Real User Monitoring - Track FCP for every visitor with our observability implementation
Lab data (Testing)
Test FCP in controlled environments:
- Lighthouse - Reports FCP with performance score impact
- PageSpeed Insights - Shows both lab and field FCP
- WebPageTest - Visual filmstrip shows exactly when FCP occurs
Visualise FCP in Chrome DevTools
// Chrome DevTools Performance tab:
// 1. Open DevTools (F12)
// 2. Go to Performance tab
// 3. Click Record, reload page, stop recording
// 4. Look for "FCP" marker in the timeline
// 5. Screenshot view shows first paint moment
// The filmstrip view shows:
// - Blank frames before FCP
// - First visible content at FCP
// - Progressive content loadingMeasure FCP programmatically
Install the web-vitals library:
npm install web-vitalsThen track FCP:
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'
delta: metric.delta,
navigationType: metric.navigationType
});
});
// Or use Paint Timing API directly
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
console.log('FCP:', entry.startTime);
}
}
});
observer.observe({ type: 'paint', buffered: true });Common causes of slow FCP
1. Render-blocking CSS
Browsers won't render until CSS loads and parses. Large stylesheets or slow-loading external CSS delay FCP significantly.
Fix: Inline critical CSS, defer non-critical styles, minimise stylesheet size.
2. Render-blocking JavaScript
JavaScript in the <head> blocks HTML parsing and rendering. Every blocking script adds 50-200ms to FCP.
Fix: Use async/defer attributes, move scripts to end of body, inline critical JS.
3. Slow server response (TTFB)
Nothing renders until the HTML arrives. High TTFB directly adds to FCP. A 2-second TTFB means FCP can't be better than 2 seconds.
Fix: Optimise server performance, use CDN, implement caching. See TTFB guide.
4. Large DOM construction
The browser must parse HTML and build the DOM before rendering. Complex HTML with thousands of elements slows this down.
Fix: Simplify HTML structure, lazy load off-screen content, server-side render critical content only.
5. Web font loading delays
With font-display: block, text doesn't render until fonts load. This creates "invisible text" and delays FCP.
Fix: Use font-display: swap or optional, preload critical fonts, use system fonts as fallbacks.
FCP optimisation techniques
FCP optimisation is mostly about your <head> section. Everything there (CSS links, scripts, font preloads) blocks the browser from rendering the first pixel. Clean up the head, and FCP improves.
1. Eliminate render-blocking resources
Defer JavaScript
<!-- Bad: Blocks HTML parsing -->
<script src="/bundle.js"></script>
<!-- Good: Defer execution -->
<script src="/bundle.js" defer></script>
<!-- Also good: Async loading (order not guaranteed) -->
<script src="/analytics.js" async></script>
<!-- Modern approach: Type module (deferred by default) -->
<script type="module" src="/app.js"></script>
Defer vs Async:
- defer: Execute in order after HTML parsing
- async: Execute immediately when loaded (random order)
- type="module": Always deferred, supports ES6 imports2. Optimise server response time (TTFB)
Fast TTFB is essential for fast FCP. See our comprehensive TTFB guide for:
- CDN edge caching
- Server-side caching strategies
- Database query optimisation
- Edge rendering for global performance
Target: TTFB under 800ms enables FCP under 1.8s
3. Preload critical resources
<!-- Preload the most critical font -->
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
<!-- Preload hero image -->
<link rel="preload" href="/hero.jpg" as="image">
<!-- Preconnect to critical third-party origins -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Priority hints for important images -->
<img src="/hero.jpg" alt="Hero" fetchpriority="high">
Warning: Don't preload too many resources
- Limit to 2-3 critical resources
- Over-preloading delays everything4. Optimise web font loading
Use font-display for instant text rendering
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
/* Show fallback immediately, swap when loaded */
font-display: swap;
}
/* Or prevent font-based layout shift */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
/* Only use font if it loads in ~100ms */
font-display: optional;
}
font-display options:
- swap: Show fallback, swap when loaded (best for FCP)
- optional: Use font only if cached (prevents layout shift)
- block: Wait for font (delays FCP, avoid this)
- fallback: 100ms block, 3s swap periodSubset fonts and use variable fonts
<!-- Bad: Load entire font family -->
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap">
<!-- Good: Load only what you need -->
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap&text=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789">
<!-- Better: Use variable font (single file, all weights) -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap">
File size comparison:
- Multiple weights (4 files): 200-400KB
- Variable font (1 file): 80-150KB
- Subset variable font: 40-80KB5. Minimise main thread work during initial load
// Bad: Heavy parsing/execution blocks FCP
import { createApp } from 'vue';
import router from './router';
import store from './store';
import components from './components'; // 200+ components
import './styles.css';
createApp(App)
.use(router)
.use(store)
.use(components)
.mount('#app');
// Good: Lazy load non-critical code
import { createApp } from 'vue';
// Minimal initial bundle
const app = createApp(App);
// Load router/store/components after FCP
requestIdleCallback(() => {
import('./router').then(({ default: router }) => {
app.use(router);
});
import('./store').then(({ default: store }) => {
app.use(store);
});
});
app.mount('#app');
Result:
- Initial bundle: 200KB -> 50KB
- Parse time: 400ms -> 80ms
- FCP improvement: 300-500ms6. Server-side render critical content
// Next.js: Server-side render for instant FCP
export async function getServerSideProps() {
const data = await fetchCriticalData();
return { props: { data } };
}
export default function Page({ data }) {
return (
<div>
{/* This HTML is sent from server, renders immediately */}
<h1>{data.title}</h1>
<p>{data.description}</p>
</div>
);
}
// Client-side rendering (CSR):
// 1. Load HTML (100ms)
// 2. Load JS (300ms)
// 3. Execute JS (200ms)
// 4. Fetch data (150ms)
// 5. Render (50ms)
// = 800ms to FCP
// Server-side rendering (SSR):
// 1. Server renders HTML (150ms)
// 2. Send HTML (50ms)
// 3. Browser renders (50ms)
// = 250ms to FCPAdvanced FCP optimisation strategies
Streaming SSR for instant FCP
// React 18: Stream HTML as it renders
import { renderToPipeableStream } from 'react-dom/server';
app.get('/', (req, res) => {
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
// Send initial HTML immediately
res.setHeader('Content-Type', 'text/html');
pipe(res);
// Browser starts rendering before full page loads
}
});
});
// Benefits:
// - FCP happens instantly (shell renders first)
// - Content streams in progressively
// - Users see something in 50-100msResource hints for faster loading
<!-- Early hints (HTTP 103) for instant resource loading -->
Link: </critical.css>; rel=preload; as=style
Link: </hero.jpg>; rel=preload; as=image
<!-- Implemented in Cloudflare Workers / Fastly -->
export default {
async fetch(request) {
return new Response(html, {
status: 200,
headers: {
'Link': '</critical.css>; rel=preload; as=style'
}
});
}
}
// Browser receives hints before HTML, starts loading immediately
// Can save 100-300ms on critical resourcesProgressive rendering techniques
<!-- Show skeleton/placeholder immediately -->
<div class="page">
<div class="skeleton-header"></div>
<div class="skeleton-content"></div>
</div>
<style>
/* Inline skeleton styles for instant FCP */
.skeleton-header {
width: 100%;
height: 60px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
animation: loading 1.5s infinite;
}
@keyframes loading {
0% { background-position: -200px 0; }
100% { background-position: 200px 0; }
}
</style>
<!-- Load actual content via JS -->
<script defer src="/app.js"></script>
Result:
- FCP: 200ms (skeleton renders)
- Meaningful paint: 800ms (content loaded)
- User sees progress immediatelyOptimise third-party scripts
<!-- Bad: Third-party scripts block FCP -->
<script src="https://www.googletagmanager.com/gtag/js"></script>
<script src="https://connect.facebook.net/en_US/sdk.js"></script>
<!-- Good: Defer all third-party scripts -->
<script src="https://www.googletagmanager.com/gtag/js" async></script>
<script src="https://connect.facebook.net/en_US/sdk.js" defer></script>
<!-- Better: Lazy load after FCP -->
<script>
// Wait for page to be interactive
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
loadAnalytics();
loadChatWidget();
});
} else {
setTimeout(() => {
loadAnalytics();
loadChatWidget();
}, 2000);
}
</script>Monitoring FCP over time
FCP can degrade as you add features. Monitor it to catch regressions early.
Track FCP with detailed context
import {onFCP} from 'web-vitals';
onFCP((metric) => {
analytics.track('fcp', {
value: metric.value,
rating: metric.rating,
page_path: location.pathname,
navigation_type: metric.navigationType,
connection_type: navigator.connection?.effectiveType,
device_memory: navigator.deviceMemory,
// Breakdown: Time to first byte
ttfb: getTTFB(),
// Detect render-blocking resources
blocking_resources: getBlockingResources()
});
});
function getBlockingResources() {
const resources = performance.getEntriesByType('resource');
return resources
.filter(r => r.renderBlockingStatus === 'blocking')
.map(r => ({ url: r.name, duration: r.duration }));
}Set up alerts
- P75 FCP threshold: Alert when 75th percentile exceeds 1.8s
- Mobile vs desktop: Track separately, mobile FCP is typically 2-3x slower
- Regression detection: Alert when FCP increases by >200ms after deployment
- Correlation analysis: Track relationship between TTFB and FCP
Performance budgets
- FCP P75: Under 1.8s globally
- TTFB: Under 800ms (enables fast FCP)
- Blocking resources: Maximum 2 render-blocking CSS files
- CSS bundle: Keep total CSS under 100KB
- Above-fold images: Preload 1-2 critical images only
FCP optimisation checklist
Need help improving your FCP?
Optimising the critical rendering path requires deep technical expertise. We can audit your resources, eliminate blocking, and implement proven solutions. Get in touch or run a free benchmark to see your current FCP.
Need help improving your FCP?
We can optimise your critical rendering path and eliminate render-blocking resources.