Fix Ghost LCP Issues | OpsBlu Docs

Fix Ghost LCP Issues

Speed up Ghost LCP by optimizing Handlebars theme images, configuring responsive srcsets, and leveraging Ghost's built-in CDN.

Largest Contentful Paint (LCP) measures how quickly the main content of a page loads. Ghost sites often struggle with LCP due to large hero images, custom fonts, and theme CSS. This guide provides Ghost-specific solutions to achieve LCP < 2.5 seconds.

Understanding LCP in Ghost

What Counts as LCP in Ghost

Common LCP elements on Ghost sites:

  • Hero Images - Large feature images on posts and pages
  • Featured Post Images - Homepage featured post images
  • Custom Header Images - Theme header backgrounds
  • First Post Card Image - Homepage post list images
  • Custom Theme Graphics - Large SVGs or illustrations

Target LCP Performance

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

Measure Current LCP

Using Google PageSpeed Insights

  1. Navigate to PageSpeed Insights
  2. Enter your Ghost site URL
  3. Click Analyze
  4. Review Largest Contentful Paint metric
  5. Click Expand view to see which element is the LCP

Using Chrome DevTools

  1. Open your Ghost site in Chrome
  2. Open DevTools (F12)
  3. Click Lighthouse tab
  4. Select Performance category
  5. Click Analyze page load
  6. Review Largest Contentful Paint in report
  7. Check View Trace to see LCP element

Using Web Vitals Extension

  1. Install Web Vitals Chrome Extension
  2. Navigate to your Ghost site
  3. Extension shows real-time LCP in toolbar
  4. Click extension icon for detailed breakdown

Ghost-Specific LCP Optimizations

1. Optimize Feature Images

Ghost feature images are often the LCP element. Optimize them aggressively.

Use Ghost's Built-In Image Optimization

Ghost automatically optimizes uploaded images, but you can enhance this:

In Ghost Admin:

  1. Navigate to Settings → Labs
  2. Enable Image Optimization (enabled by default in Ghost 5.x)
  3. Re-upload large images to trigger optimization

Image Upload Best Practices:

  • Upload images at target display size (max 2000px wide for most themes)
  • Use JPEG for photos (smaller file size)
  • Use WebP format when possible (Ghost 5.x auto-converts)
  • Compress images before upload using tools like TinyPNG

Responsive Images in Ghost Themes

Ghost provides \{\{img_url\}\} helper for responsive images:

{{!-- In post.hbs or page.hbs --}}
{{#if feature_image}}
  <img
    srcset="{{img_url feature_image size="s"}} 300w,
            {{img_url feature_image size="m"}} 600w,
            {{img_url feature_image size="l"}} 1000w,
            {{img_url feature_image size="xl"}} 2000w"
    sizes="(max-width: 600px) 300px,
           (max-width: 1000px) 600px,
           (max-width: 1600px) 1000px,
           2000px"
    src="{{img_url feature_image size="xl"}}"
    alt="{{title}}"
  >
{{/if}}

Sizes Reference:

  • s: ~300px wide
  • m: ~600px wide
  • l: ~1000px wide
  • xl: ~2000px wide

Lazy Load Non-LCP Images

Only lazy load images below the fold, not the LCP image:

{{!-- Don't lazy load feature image (it's LCP) --}}
{{#if feature_image}}
  <img
    src="{{img_url feature_image size="xl"}}"
    alt="{{title}}"
    {{!-- No loading="lazy" on LCP image --}}
  >
{{/if}}

{{!-- Lazy load images in post content --}}
<div class="post-content">
  {{{content}}}
</div>

<script>
  // Add loading="lazy" to images in content (not LCP)
  document.querySelectorAll('.post-content img').forEach(function(img) {
    img.setAttribute('loading', 'lazy');
  });
</script>

Preload LCP Image

Tell browser to prioritize LCP image:

{{!-- In default.hbs or post.hbs --}}
<head>
  {{#post}}
    {{#if feature_image}}
      <link rel="preload" as="image" href="{{img_url feature_image size="xl"}}">
    {{/if}}
  {{/post}}

  {{ghost_head}}
</head>

Note: Only preload the actual LCP image. Preloading too many images hurts performance.

2. Optimize Ghost Theme CSS

CSS blocks rendering until fully downloaded and parsed. Optimize theme stylesheets.

Inline Critical CSS

Extract and inline CSS needed for above-the-fold content:

{{!-- In default.hbs --}}
<head>
  {{!-- Inline critical CSS --}}
  <style>
    /* Critical above-the-fold styles */
    body {
      margin: 0;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    }
    .site-header {
      background: #fff;
      padding: 20px;
    }
    .post-feature-image {
      width: 100%;
      height: auto;
    }
    /* Add other critical styles */
  </style>

  {{!-- Load full stylesheet asynchronously --}}
  <link rel="preload" href="{{asset "built/screen.css"}}" as="style"
  <noscript><link rel="stylesheet" href="{{asset "built/screen.css"}}"></noscript>

  {{ghost_head}}
</head>

Generate Critical CSS:

Remove Unused CSS

Ghost themes may include CSS for features you don't use:

# Install PurgeCSS
npm install -g purgecss

# Analyze Ghost theme CSS
purgecss --css assets/built/screen.css --content *.hbs **/*.hbs --output assets/built/

Then include the purged CSS in your theme.

Defer Non-Critical CSS

{{!-- Defer font stylesheet --}}
<link rel="preload" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" as="style"
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap"></noscript>

3. Optimize Web Fonts

Custom fonts can significantly delay LCP. Optimize font loading.

Use System Fonts

Fastest option - no network request:

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

Self-Host Google Fonts

Reduces DNS lookup and connection time:

  1. Download fonts from Google Webfonts Helper
  2. Upload to Ghost theme: /assets/fonts/
  3. Add to theme CSS:
/* In assets/css/screen.css */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/inter-v12-latin-regular.woff2') format('woff2');
}

body {
  font-family: 'Inter', sans-serif;
}

Use font-display: swap

Prevents invisible text while font loads:

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

Preload Critical Fonts

<head>
  <link rel="preload" href="{{asset "fonts/inter-regular.woff2"}}" as="font" type="font/woff2" crossorigin>
  {{ghost_head}}
</head>

4. CDN and Caching Configuration

Ghost(Pro) CDN Optimization

Ghost(Pro) uses Cloudflare CDN automatically:

No configuration needed - Ghost(Pro) handles this automatically.

Self-Hosted CDN Setup

For self-hosted Ghost, implement CDN manually:

Option 1: Cloudflare (Free)

  1. Create Cloudflare account
  2. Add your domain
  3. Update nameservers
  4. Enable settings:
    • Auto Minify - HTML, CSS, JS
    • Brotli compression - ON
    • Rocket Loader - OFF (conflicts with tracking)
    • Polish - Lossy (WebP image optimization)

Option 2: BunnyCDN or Other CDN

Configure Ghost to use CDN for assets:

// In Ghost config.production.json
{
  "url": "https://yourdomain.com",
  "storage": {
    "active": "ghost-storage-adapter-s3",
    "ghost-storage-adapter-s3": {
      "assetHost": "https://cdn.yourdomain.com"
    }
  }
}

Cache-Control Headers

For self-hosted Ghost with Nginx:

# In nginx.conf
location ~* \.(jpg|jpeg|png|gif|webp|svg|woff|woff2)$ {
  expires 1y;
  add_header Cache-Control "public, immutable";
}

location ~* \.(css|js)$ {
  expires 1y;
  add_header Cache-Control "public, immutable";
}

5. Reduce Server Response Time (TTFB)

Slow server response delays LCP. Optimize Ghost server performance.

For Ghost(Pro)

Ghost(Pro) handles server optimization automatically. If TTFB is high:

  1. Contact Ghost support
  2. Check if on shared vs. dedicated instance
  3. Consider upgrading plan for better performance

For Self-Hosted Ghost

Optimize Node.js:

// In Ghost config.production.json
{
  "server": {
    "host": "0.0.0.0",
    "port": 2368
  },
  "database": {
    "client": "mysql",
    "connection": {
      "host": "localhost",
      "port": 3306,
      "database": "ghost_prod",
      "pool": {
        "min": 2,
        "max": 10
      }
    }
  }
}

Use Redis Caching:

# Install Redis
apt-get install redis-server

# Configure Ghost to use Redis
npm install ghost-storage-adapter-redis
// In config.production.json
{
  "caching": {
    "adapter": "redis",
    "host": "localhost",
    "port": 6379
  }
}

Optimize MySQL/MariaDB:

-- Analyze slow queries
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;

-- Optimize tables
OPTIMIZE TABLE posts;
OPTIMIZE TABLE users;
OPTIMIZE TABLE tags;

Upgrade Server Resources:

  • Minimum: 1GB RAM, 1 CPU core
  • Recommended: 2GB RAM, 2 CPU cores
  • High traffic: 4GB+ RAM, 4+ CPU cores

6. Remove Render-Blocking Resources

Defer Third-Party Scripts

Move non-critical scripts to load after LCP:

{{!-- Bad: Blocks rendering --}}
<script src="https://cdn.example.com/widget.js"></script>

{{!-- Good: Deferred loading --}}
<script defer src="https://cdn.example.com/widget.js"></script>

{{!-- Better: Load after LCP --}}
<script>
  window.addEventListener('load', function() {
    var script = document.createElement('script');
    script.src = 'https://cdn.example.com/widget.js';
    document.body.appendChild(script);
  });
</script>

Optimize Analytics Scripts

{{!-- Defer GA4 --}}
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>

{{!-- Defer GTM --}}
<script>
  (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
  new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
  j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
  'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
  })(window,document,'script','dataLayer','GTM-XXXXXXX');
</script>

7. Ghost Portal Optimization

Ghost Portal can impact LCP if it loads synchronously.

Defer Portal Loading

{{!-- In default.hbs --}}
<script>
  // Defer Portal initialization
  if (window.ghost) {
    window.ghost.init = function() {
      requestIdleCallback(function() {
        // Initialize Portal after LCP
      });
    };
  }
</script>

Note: This may affect member functionality. Test thoroughly.

Advanced Optimizations

Implement Early Hints (HTTP 103)

For self-hosted Ghost with compatible server:

# Nginx config (requires nginx 1.21.8+)
location / {
  add_header Link "</assets/built/screen.css>; rel=preload; as=style" always;
  add_header Link "</assets/fonts/font.woff2>; rel=preload; as=font; crossorigin" always;
}

Use Priority Hints

{{#if feature_image}}
  <img
    src="{{img_url feature_image size="xl"}}"
    alt="{{title}}"
    fetchpriority="high"
    {{!-- Tells browser this image is high priority --}}
  >
{{/if}}

Implement Resource Hints

<head>
  {{!-- DNS Prefetch for external domains --}}
  <link rel="dns-prefetch" href="//fonts.googleapis.com">
  <link rel="dns-prefetch" href="//www.google-analytics.com">

  {{!-- Preconnect to critical origins --}}
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

  {{!-- Preload LCP image --}}
  {{#post}}
    {{#if feature_image}}
      <link rel="preload" as="image" href="{{img_url feature_image size="xl"}}">
    {{/if}}
  {{/post}}

  {{ghost_head}}
</head>

Testing and Monitoring

Continuous Monitoring

Set up automated LCP monitoring:

Google Search Console:

  1. Navigate to Experience → Core Web Vitals
  2. Review LCP issues by URL
  3. Click Open Report for details

Lighthouse CI:

# Install Lighthouse CI
npm install -g @lhci/cli

# Run audit
lhci autorun --collect.url=https://yoursite.com

Web Vitals Monitoring:

// Add to Ghost theme
import {getLCP} from 'web-vitals';

getLCP(function(metric) {
  // Send to analytics
  gtag('event', 'web_vitals', {
    event_category: 'Web Vitals',
    event_label: metric.id,
    value: Math.round(metric.value),
    metric_name: 'LCP'
  });
});

Before and After Comparison

Document improvements:

  1. Baseline Measurement:

    • Run PageSpeed Insights before optimization
    • Record LCP score
    • Note LCP element
  2. Implement Optimizations:

    • Apply changes systematically
    • Test each change individually
  3. Measure Results:

    • Run PageSpeed Insights after each change
    • Compare LCP scores
    • Verify LCP element hasn't changed

Common LCP Issues in Ghost Themes

Issue: Large Hero Image

Problem: 2MB+ feature image as LCP Solution:

  • Resize image to max 2000px width
  • Compress with TinyPNG
  • Convert to WebP format
  • Use responsive images with srcset

Issue: Custom Fonts Blocking Render

Problem: LCP delayed by web font loading Solution:

  • Use font-display: swap
  • Preload critical font files
  • Or switch to system fonts

Issue: Unoptimized Theme CSS

Problem: Large CSS file blocks rendering Solution:

  • Inline critical CSS
  • Defer non-critical CSS
  • Remove unused styles

Issue: Slow Server Response

Problem: TTFB > 600ms Solution:

  • Enable Redis caching
  • Optimize database queries
  • Upgrade server resources
  • Use CDN

Next Steps