All writing

Web Performance / Digital Optimization

Optimizing LCP for Better Web Performance

3 min read Roland Farkas
Graph showing LCP improvement from 3.6s to 1.7s

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: swap to optional
  • 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

MetricBeforeAfterImprovement
LCP3.6s1.7s-52.8% 🎉
Performance Score89100+11 points
FCP1.5s1.2s-0.3s

Key Takeaways

  1. Only preload critical resources - Don’t preload everything
  2. Use font-display: optional - Better than swap for LCP
  3. Add static HTML - Don’t wait for JavaScript to show content
  4. Inline critical CSS - Eliminate render-blocking stylesheets
  5. 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

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.

#LCP#Core Web Vitals#Performance#PageSpeed#Fonts