Fix Netlify CMS / Decap CMS LCP Issues | OpsBlu Docs

Fix Netlify CMS / Decap CMS LCP Issues

Improve LCP on Netlify CMS static sites by optimizing build-time image processing, preloading hero assets, and trimming third-party scripts.

Largest Contentful Paint (LCP) measures how long it takes for the main content to load. Static sites built with Netlify CMS should inherently be fast, but tracking scripts, images, and third-party resources can slow down LCP.

What is LCP?

LCP measures when the largest visible content element renders.

Thresholds:

  • Good: < 2.5 seconds
  • Needs Improvement: 2.5 - 4.0 seconds
  • Poor: > 4.0 seconds

Common LCP Elements on Static Sites:

  • Hero images (homepage banners, featured images)
  • Large text blocks (blog post titles, headings)
  • Video thumbnails
  • Code blocks in documentation sites

Measuring LCP on Static Sites

PageSpeed Insights

  1. Go to PageSpeed Insights
  2. Enter your site URL (production or preview deploy)
  3. View Largest Contentful Paint metric
  4. Check Diagnostics for specific issues

Lighthouse CLI

# Install Lighthouse
npm install -g lighthouse

# Run audit
lighthouse https://yoursite.com --view

# Mobile-specific
lighthouse https://yoursite.com --preset=mobile --view

WebPageTest

  1. Go to WebPageTest
  2. Enter URL
  3. Select location nearest your audience
  4. Choose device type (Mobile/Desktop)
  5. Run test
  6. View LCP in filmstrip and metrics

Common Causes of Slow LCP on Static Sites

1. Unoptimized Images

Symptoms:

  • Large hero images (> 500 KB)
  • Images not using modern formats (WebP, AVIF)
  • Images larger than display size
  • No lazy loading exclusions for LCP image

Solutions:

Use Static Site Generator Image Optimization

Hugo:

<!-- layouts/_default/baseof.html -->
{{ $image := resources.Get "images/hero.jpg" }}
{{ $webp := $image.Resize "1920x webp q85" }}
{{ $fallback := $image.Resize "1920x jpg q85" }}

<picture>
  <source srcset="{{ $webp.RelPermalink }}" type="image/webp">
  <img src="{{ $fallback.RelPermalink }}" alt="Hero" loading="eager" fetchpriority="high">
</picture>

Gatsby:

// Install plugin
npm install gatsby-plugin-image gatsby-plugin-sharp gatsby-transformer-sharp

// gatsby-config.js
{
  resolve: `gatsby-plugin-image`,
  options: {
    defaults: {
      formats: [`auto`, `webp`, `avif`],
      quality: 85,
    }
  }
}

// Component usage
import { StaticImage } from 'gatsby-plugin-image'

<StaticImage
  src="../images/hero.jpg"
  alt="Hero"
  loading="eager"
  placeholder="blurred"
  width={1920}
  height={1080}
/>

Next.js:

// next.config.js
module.exports = {
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920],
  },
};

// Component
import Image from 'next/image';

<Image
  src="/images/hero.jpg"
  alt="Hero"
  width={1920}
  height={1080}
  priority // Don't lazy load LCP image!
  quality={85}
/>

Use Image CDN

Cloudinary:

<!-- Auto-format, auto-quality -->
<img src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto/hero.jpg"
     loading="eager"
     fetchpriority="high"
     alt="Hero">

imgix:

<img src="https://yoursite.imgix.net/hero.jpg?auto=format,compress&w=1920"
     loading="eager"
     fetchpriority="high"
     alt="Hero">

Exclude LCP Image from Lazy Loading

Hugo:

{{ if .IsHome }}
  <!-- Homepage hero - no lazy loading -->
  <img src="{{ $hero.RelPermalink }}" loading="eager" fetchpriority="high" alt="Hero">
{{ else }}
  <!-- Other images - lazy load -->
  <img src="{{ .Src }}" loading="lazy" alt="{{ .Alt }}">
{{ end }}

Jekyll:

{% if page.url == "/" %}
  <img src="{{ site.hero_image }}" loading="eager" fetchpriority="high" alt="Hero">
{% else %}
  <img src="{{ include.src }}" loading="lazy" alt="{{ include.alt }}">
{% endif %}

2. Render-Blocking Resources

Symptoms:

  • Large CSS files (> 100 KB)
  • Synchronous JavaScript in head
  • Web fonts loading synchronously
  • Multiple third-party scripts

Solutions:

Critical CSS Inline

Hugo:

<!-- layouts/partials/critical-css.html -->
<style>
  {{ $critical := resources.Get "css/critical.css" }}
  {{ $critical.Content | safeCSS }}
</style>

<!-- Load full CSS asynchronously -->
{{ $css := resources.Get "css/main.css" | minify | fingerprint }}
<link rel="preload" href="{{ $css.RelPermalink }}" as="style"
<noscript><link rel="stylesheet" href="{{ $css.RelPermalink }}"></noscript>

Gatsby:

# Install plugin
npm install gatsby-plugin-inline-critical-css

# gatsby-config.js
{
  resolve: `gatsby-plugin-inline-critical-css`,
  options: {
    ignore: {
      atrule: ['@font-face'],
    },
  },
}

Defer Non-Critical JavaScript

Universal approach:

<!-- Defer analytics scripts -->
<script defer src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script defer>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXXXXX');
</script>

Optimize Web Fonts

Preload critical fonts:

<link rel="preload" href="/fonts/primary-font.woff2" as="font" type="font/woff2" crossorigin>

Use font-display: swap:

@font-face {
  font-family: 'YourFont';
  src: url('/fonts/your-font.woff2') format('woff2');
  font-display: swap; /* Show fallback text immediately */
}

System fonts (fastest):

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
}

3. Slow Server Response (TTFB)

Symptoms:

Solutions:

Use Netlify CDN Properly

Ensure proper headers:

# netlify.toml
[[headers]]
  for = "/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

[[headers]]
  for = "/*.html"
  [headers.values]
    Cache-Control = "public, max-age=0, must-revalidate"

Optimize Build Output

Hugo:

# config.toml
[minify]
  minifyOutput = true

[build]
  writeStats = true

Gatsby:

// gatsby-config.js
module.exports = {
  flags: {
    FAST_DEV: true,
    PARALLEL_SOURCING: true,
  },
};

Next.js:

// next.config.js
module.exports = {
  compress: true,
  swcMinify: true,
  compiler: {
    removeConsole: process.env.NODE_ENV === 'production',
  },
};

4. Third-Party Scripts (Analytics, Pixels)

Symptoms:

  • GTM container blocks rendering
  • GA4/Meta Pixel loads synchronously
  • Chat widgets load on initial page load

Solutions:

Delay Third-Party Scripts

// Load analytics after user interaction
let analyticsLoaded = false;

function loadAnalytics() {
  if (analyticsLoaded) return;
  analyticsLoaded = true;

  // Load GTM
  (function(w,d,s,l,i){...})(window,document,'script','dataLayer','GTM-XXXXXX');
}

// Trigger on first interaction
['mousedown', 'touchstart', 'keydown', 'scroll'].forEach(event => {
  window.addEventListener(event, loadAnalytics, {once: true, passive: true});
});

// Fallback after 3 seconds
setTimeout(loadAnalytics, 3000);

Use Resource Hints

<link rel="preconnect" href="https://www.google-analytics.com">
<link rel="preconnect" href="https://www.googletagmanager.com">
<link rel="dns-prefetch" href="//connect.facebook.net">

Conditional Loading

Hugo:

{{ if eq (getenv "CONTEXT") "production" }}
  {{ partial "analytics.html" . }}
{{ end }}

Gatsby:

// gatsby-config.js
{
  resolve: `gatsby-plugin-google-gtag`,
  options: {
    includeInDevelopment: false, // Only load in production
  }
}

5. Unoptimized Static Site Generator Build

Symptoms:

  • Slow build times (> 5 minutes)
  • Large bundle sizes
  • Unused CSS/JS in output

Solutions:

Hugo Optimization

# config.toml
[build]
  useResourceCacheWhen = "always"

[minify]
  disableHTML = false
  disableCSS = false
  disableJS = false
  disableJSON = false
  disableSVG = false
  disableXML = false
  minifyOutput = true

Gatsby Optimization

# Use incremental builds
GATSBY_EXPERIMENTAL_PAGE_BUILD_ON_DATA_CHANGES=true gatsby build --log-pages

# Parallel image processing
npm install gatsby-plugin-sharp@latest

Next.js Optimization

// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  swcMinify: true,
  compiler: {
    removeConsole: process.env.NODE_ENV === 'production',
  },
  experimental: {
    optimizeCss: true,
  },
});

Static Site-Specific Optimizations

Preload LCP Image

<!-- Hugo -->
{{ if .IsHome }}
  {{ $hero := resources.Get "images/hero.jpg" }}
  <link rel="preload" as="image" href="{{ $hero.RelPermalink }}">
{{ end }}

<!-- Next.js - use priority prop on Image component -->
<Image src="/hero.jpg" priority />

<!-- Gatsby - use loading="eager" -->
<StaticImage src="../hero.jpg" loading="eager" />

Eliminate Render-Blocking Resources

Inline critical CSS, defer rest:

<style>
  /* Critical above-the-fold CSS */
  .hero { ... }
  .header { ... }
</style>

<link rel="preload" href="/css/full.css" as="style"
<noscript><link rel="stylesheet" href="/css/full.css"></noscript>

Optimize Content Images

Responsive images:

<!-- Hugo -->
{{ $image := resources.Get .Src }}
{{ $small := $image.Resize "640x webp q85" }}
{{ $medium := $image.Resize "1024x webp q85" }}
{{ $large := $image.Resize "1920x webp q85" }}

<img
  srcset="{{ $small.RelPermalink }} 640w,
          {{ $medium.RelPermalink }} 1024w,
          {{ $large.RelPermalink }} 1920w"
  sizes="(max-width: 640px) 640px, (max-width: 1024px) 1024px, 1920px"
  src="{{ $large.RelPermalink }}"
  alt="{{ .Alt }}"
  loading="lazy">

Testing LCP Improvements

Before/After Comparison

  1. Baseline: Run Lighthouse before changes
  2. Make optimizations
  3. Re-test: Clear cache, run Lighthouse again
  4. Compare: Note LCP improvement

Test on Preview Deploy

# Make changes in branch
git checkout -b optimize-lcp

# Commit changes
git add .
git commit -m "Optimize LCP"
git push origin optimize-lcp

# Create pull request
# Netlify generates preview deploy
# Test preview deploy URL with Lighthouse

Monitor Real Users

Use GA4 to track real-user LCP:

// Report Core Web Vitals to GA4
import {getCLS, getFID, getFCP, getLCP, getTTFB} from 'web-vitals';

function sendToAnalytics({name, delta, id}) {
  gtag('event', name, {
    event_category: 'Web Vitals',
    value: Math.round(name === 'CLS' ? delta * 1000 : delta),
    event_label: id,
    non_interaction: true,
  });
}

getLCP(sendToAnalytics);

Checklist for LCP Optimization

  • Optimize LCP image - WebP/AVIF format, proper sizing
  • Don't lazy load LCP image - Use loading="eager" fetchpriority="high"
  • Preload LCP image - Resource hint for critical image
  • Inline critical CSS - Above-the-fold styles in <head>
  • Defer non-critical JS - Analytics, chat widgets
  • Optimize web fonts - Preload, font-display: swap
  • Use CDN - Netlify Edge for fast global delivery
  • Minimize third-party scripts - Load after interaction
  • Enable compression - Gzip/Brotli on Netlify
  • Optimize build output - Minify HTML/CSS/JS

Next Steps