Introduction
Largest Contentful Paint (LCP) is one of the most critical Core Web Vitals metrics. In this comprehensive guide, I’ll share how I reduced LCP from 3.6 seconds to under 2.0 seconds on a production website.
The Problem
When analyzing the site with PageSpeed Insights, I discovered several performance bottlenecks:
- Font loading blocking text rendering (1.2-1.8s delay)
- React hydration requiring JS download before content appeared
- CSS render blocking the initial paint
- Network waterfall creating serial dependencies
Quick Tip: Always test on mobile with PageSpeed Insights. Desktop scores can be misleading!
Root Cause Analysis
1. Font Loading Strategy
The original implementation preloaded all 4 font weights (186KB total):
<!-- ❌ BAD: Preloading all fonts -->
<link rel="preload" href="/fonts/montserrat-700.woff2" as="font" crossorigin />
<link rel="preload" href="/fonts/montserrat-400.woff2" as="font" crossorigin />
<link rel="preload" href="/fonts/montserrat-500.woff2" as="font" crossorigin />
<link rel="preload" href="/fonts/montserrat-600.woff2" as="font" crossorigin />
This caused the LCP element (the <h1>) to wait for all fonts before rendering.
2. Missing Static HTML
The React app rendered into an empty div:
<!-- ❌ BAD: Empty root, waits for JS -->
<div id="root"></div>
Users saw a blank page until JavaScript loaded, parsed, and executed.
The Solution
Step 1: Optimize Font Loading
I changed the strategy to only preload the critical font needed for the LCP element:
<!-- ✅ GOOD: Preload only critical font -->
<link rel="preload" href="/fonts/montserrat-700.woff2" as="font" crossorigin />
<!-- Size-adjusted fallback prevents layout shift -->
<style>
@font-face {
font-family: 'Montserrat Fallback';
src: local('Arial');
size-adjust: 105.8%;
ascent-override: 87%;
descent-override: 22%;
}
@font-face {
font-family: 'Montserrat';
font-weight: 700;
font-display: optional; /* Key change! */
src: url('/fonts/montserrat-700.woff2') format('woff2');
}
</style>
Key changes:
- Preload only 1 font (47KB instead of 186KB)
- Changed
font-display: swaptooptional - Added size-adjusted system font fallback
Step 2: Add Static HTML Skeleton
Added critical content directly in the HTML:
<!-- ✅ GOOD: Static HTML for instant rendering -->
<div id="root">
<section class="hero">
<h1 class="hero-title">
Digital Performance Consultant
</h1>
<p>High-impact consulting for companies...</p>
</section>
</div>
React hydrates over this, but content is visible immediately!
Step 3: Inline Critical CSS
<style>
.hero-title {
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 700;
line-height: 1.1;
}
</style>
This ensures the hero section styles are available without waiting for the CSS file.
Results
| Metric | Before | After | Improvement |
|---|---|---|---|
| LCP | 3.6s | 1.7s | -52.8% 🎉 |
| Performance Score | 89 | 100 | +11 points |
| FCP | 1.5s | 1.2s | -0.3s |
Key Takeaways
- Only preload critical resources - Don’t preload everything
- Use
font-display: optional- Better thanswapfor LCP - Add static HTML - Don’t wait for JavaScript to show content
- Inline critical CSS - Eliminate render-blocking stylesheets
- Size-adjusted fallbacks - Prevent layout shift when fonts load
Implementation Checklist
- Identify your LCP element (use PageSpeed Insights)
- Determine which font it uses
- Preload only that font
- Change to
font-display: optional - Add size-adjusted fallback
- Add static HTML for above-the-fold content
- Inline critical CSS
- Test with PageSpeed Insights
- Monitor with Real User Monitoring
Advanced Tips
Subset Your Fonts
Reduce font size even further by including only used characters:
pyftsubset fonts/montserrat-700.woff2 \\
--text="Your exact text content" \\
--output-file=fonts/montserrat-700-subset.woff2
This can reduce a 47KB font to ~20KB!
Use Variable Fonts
Instead of 4 separate font files (186KB), use one variable font (~60KB):
@font-face {
font-family: 'Montserrat';
src: url('/fonts/montserrat-variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-display: optional;
}
Tools & Resources
- PageSpeed Insights - Test your site
- Web Vitals Chrome Extension - Monitor metrics
- Fonttools - Subset fonts
- Google Fonts Helper - Self-host Google Fonts
Conclusion
Optimizing LCP requires a holistic approach. By focusing on font loading strategy, static HTML rendering, and critical CSS inlining, I achieved a 52.8% reduction in LCP time.
The key is to prioritize what’s needed for the initial paint and defer everything else.