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
- Navigate to PageSpeed Insights
- Enter your Ghost site URL
- Click Analyze
- Review Largest Contentful Paint metric
- Click Expand view to see which element is the LCP
Using Chrome DevTools
- Open your Ghost site in Chrome
- Open DevTools (F12)
- Click Lighthouse tab
- Select Performance category
- Click Analyze page load
- Review Largest Contentful Paint in report
- Check View Trace to see LCP element
Using Web Vitals Extension
- Install Web Vitals Chrome Extension
- Navigate to your Ghost site
- Extension shows real-time LCP in toolbar
- 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:
- Navigate to Settings → Labs
- Enable Image Optimization (enabled by default in Ghost 5.x)
- 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 widem: ~600px widel: ~1000px widexl: ~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:
- Use Critical tool
- Or Critical CSS Generator
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:
- Download fonts from Google Webfonts Helper
- Upload to Ghost theme:
/assets/fonts/ - 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:
- Images cached at edge locations worldwide
- Automatic image optimization (WebP conversion)
- Brotli compression for text assets
- HTTP/3 support
No configuration needed - Ghost(Pro) handles this automatically.
Self-Hosted CDN Setup
For self-hosted Ghost, implement CDN manually:
Option 1: Cloudflare (Free)
- Create Cloudflare account
- Add your domain
- Update nameservers
- Enable settings:
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:
- Contact Ghost support
- Check if on shared vs. dedicated instance
- 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:
- Navigate to Experience → Core Web Vitals
- Review LCP issues by URL
- 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:
Baseline Measurement:
- Run PageSpeed Insights before optimization
- Record LCP score
- Note LCP element
Implement Optimizations:
- Apply changes systematically
- Test each change individually
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
- CLS Optimization - Fix layout shift issues
- Tracking Events Not Firing - Debug tracking
- Ghost Integrations - Optimize integration performance