Largest Contentful Paint (LCP) measures how quickly the main content of your Contentful-powered site loads. Poor LCP directly impacts SEO rankings and conversion rates.
Target: LCP under 2.5 seconds Good: Under 2.5s | Needs Improvement: 2.5-4.0s | Poor: Over 4.0s
For general LCP concepts, see the global LCP guide.
Contentful-Specific LCP Issues
1. Unoptimized Contentful Images
The most common LCP issue is large, unoptimized images from Contentful's Image API.
Problem: Hero images or featured images loading at full resolution.
Diagnosis:
- Run PageSpeed Insights
- Check if Contentful image is the LCP element
- Look for "Properly size images" warning
Solutions:
A. Use Contentful Image API Parameters
Contentful's Image API supports dynamic image transformation:
// Helper function for Contentful images
export function getContentfulImageUrl(
url: string,
options: {
width?: number
height?: number
quality?: number
format?: 'webp' | 'jpg' | 'png'
fit?: 'pad' | 'fill' | 'scale' | 'crop' | 'thumb'
focus?: 'center' | 'top' | 'right' | 'left' | 'bottom' | 'top_right' | 'top_left' | 'bottom_right' | 'bottom_left' | 'face' | 'faces'
} = {}
): string {
const params = new URLSearchParams()
if (options.width) params.append('w', options.width.toString())
if (options.height) params.append('h', options.height.toString())
if (options.quality) params.append('q', options.quality.toString())
if (options.format) params.append('fm', options.format)
if (options.fit) params.append('fit', options.fit)
if (options.focus) params.append('f', options.focus)
return `${url}?${params.toString()}`
}
// Usage in component
export function HeroImage({ asset }) {
const imageUrl = getContentfulImageUrl(asset.fields.file.url, {
width: 1920,
quality: 80,
format: 'webp',
fit: 'fill',
})
return (
<img
src={imageUrl}
alt={asset.fields.title}
width={1920}
height={1080}
loading="eager"
/>
)
}
B. Implement Responsive Images
Use srcset for different screen sizes:
export function ResponsiveContentfulImage({ asset, alt }) {
const baseUrl = asset.fields.file.url
return (
<img
src={getContentfulImageUrl(baseUrl, { width: 1920, format: 'webp' })}
srcSet={`
${getContentfulImageUrl(baseUrl, { width: 640, format: 'webp' })} 640w,
${getContentfulImageUrl(baseUrl, { width: 750, format: 'webp' })} 750w,
${getContentfulImageUrl(baseUrl, { width: 828, format: 'webp' })} 828w,
${getContentfulImageUrl(baseUrl, { width: 1080, format: 'webp' })} 1080w,
${getContentfulImageUrl(baseUrl, { width: 1200, format: 'webp' })} 1200w,
${getContentfulImageUrl(baseUrl, { width: 1920, format: 'webp' })} 1920w,
`}
sizes="100vw"
alt={alt || asset.fields.title}
width={asset.fields.file.details.image.width}
height={asset.fields.file.details.image.height}
loading="eager"
/>
)
}
C. Next.js Image Component
Use Next.js Image component with Contentful:
import Image from 'next/image'
// Configure Contentful domain in next.config.js
// next.config.js
module.exports = {
images: {
domains: ['images.ctfassets.net'],
formats: ['image/webp'],
},
}
// Component
export function ContentfulImage({ asset, priority = false }) {
return (
<Image
src={`https:${asset.fields.file.url}`}
alt={asset.fields.title}
width={asset.fields.file.details.image.width}
height={asset.fields.file.details.image.height}
priority={priority} // true for LCP image
quality={80}
placeholder="blur"
blurDataURL={getBlurDataURL(asset)}
/>
)
}
D. Generate Blur Placeholders
Create low-quality placeholders for better perceived performance:
// Generate blur data URL from Contentful
export function getBlurDataURL(asset: any): string {
const tinyUrl = getContentfulImageUrl(asset.fields.file.url, {
width: 10,
quality: 10,
})
return tinyUrl
}
// Or use a library
import { getPlaiceholder } from 'plaiceholder'
export async function getStaticProps() {
const post = await getContentfulPost()
const { base64 } = await getPlaiceholder(
`https:${post.fields.featuredImage.fields.file.url}`
)
return {
props: {
post,
blurDataURL: base64,
},
}
}
2. Slow Contentful API Responses
Waiting for Contentful API on the client causes LCP delays.
Problem: Content fetched on client-side, blocking render.
Diagnosis:
- Check Network tab for Contentful API calls
- Measure time to first byte (TTFB)
- Look for API calls blocking render
Solutions:
A. Use Static Site Generation (SSG)
Pre-render pages at build time:
Next.js:
// pages/blog/[slug].tsx
export async function getStaticProps({ params }) {
const post = await contentfulClient.getEntries({
content_type: 'blogPost',
'fields.slug': params.slug,
})
return {
props: {
post: post.items[0],
},
revalidate: 3600, // Rebuild every hour
}
}
export async function getStaticPaths() {
const posts = await contentfulClient.getEntries({
content_type: 'blogPost',
})
return {
paths: posts.items.map(post => ({
params: { slug: post.fields.slug },
})),
fallback: 'blocking',
}
}
// gatsby-node.js
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
const result = await graphql(`
query {
allContentfulBlogPost {
nodes {
slug
}
}
}
`)
result.data.allContentfulBlogPost.nodes.forEach(post => {
createPage({
path: `/blog/${post.slug}`,
component: require.resolve('./src/templates/blog-post.tsx'),
context: { slug: post.slug },
})
})
}
B. Implement Caching
Cache Contentful responses:
// Simple in-memory cache
const cache = new Map()
export async function getCachedContentfulEntry(id: string) {
const cacheKey = `entry_${id}`
if (cache.has(cacheKey)) {
return cache.get(cacheKey)
}
const entry = await contentfulClient.getEntry(id)
cache.set(cacheKey, entry)
// Clear cache after 5 minutes
setTimeout(() => cache.delete(cacheKey), 5 * 60 * 1000)
return entry
}
// Redis cache for production
import { createClient } from 'redis'
const redis = createClient({ url: process.env.REDIS_URL })
export async function getContentfulEntryWithRedis(id: string) {
const cacheKey = `contentful:entry:${id}`
const cached = await redis.get(cacheKey)
if (cached) {
return JSON.parse(cached)
}
const entry = await contentfulClient.getEntry(id)
// Cache for 1 hour
await redis.setEx(cacheKey, 3600, JSON.stringify(entry))
return entry
}
C. Use GraphQL for Efficiency
GraphQL fetches only needed fields:
// With REST API - fetches everything
const entry = await contentfulClient.getEntry(id)
// With GraphQL - only what you need
const query = `
query {
blogPost(id: "${id}") {
title
slug
publishDate
featuredImage {
url
width
height
}
}
}
`
const { data } = await fetch(
`https://graphql.contentful.com/content/v1/spaces/${SPACE_ID}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${ACCESS_TOKEN}`,
},
body: JSON.stringify({ query }),
}
).then(res => res.json())
3. Client-Side Rendering (CSR) Delays
CSR requires additional round trips before rendering content.
Problem: Page waits for JavaScript, then fetches content, then renders.
Diagnosis:
- Check if content appears after JavaScript loads
- Look for "white screen" before content
- Measure time to interactive (TTI)
Solutions:
A. Use Server-Side Rendering (SSR)
Render content on the server:
Next.js:
// pages/blog/[slug].tsx
export async function getServerSideProps({ params }) {
const post = await contentfulClient.getEntries({
content_type: 'blogPost',
'fields.slug': params.slug,
})
return {
props: {
post: post.items[0],
},
}
}
Nuxt:
<script>
export default {
async asyncData({ $contentful, params }) {
const post = await $contentful.getEntries({
content_type: 'blogPost',
'fields.slug': params.slug,
})
return {
post: post.items[0],
}
},
}
</script>
B. Implement Streaming SSR
Next.js (App Router):
// app/blog/[slug]/page.tsx
import { Suspense } from 'react'
async function BlogContent({ slug }) {
const post = await getContentfulPost(slug)
return <article>{/* Content */}</article>
}
export default function BlogPage({ params }) {
return (
<Suspense fallback={<LoadingSkeleton />}>
<BlogContent slug={params.slug} />
</Suspense>
)
}
4. Large Rich Text Fields
Processing large Rich Text content on the client is slow.
Problem: Rich Text rendering blocks paint.
Diagnosis:
- Check if LCP element is in Rich Text
- Profile Rich Text processing time
- Look for multiple embedded entries
Solutions:
A. Process Rich Text on Server
// Server-side
import { documentToHtmlString } from '@contentful/rich-text-html-renderer'
export async function getStaticProps() {
const post = await getContentfulPost()
// Process on server
const bodyHtml = documentToHtmlString(post.fields.body)
return {
props: {
post,
bodyHtml,
},
}
}
// Client-side
export default function BlogPost({ bodyHtml }) {
return <div dangerouslySetInnerHTML={{ __html: bodyHtml }} />
}
B. Lazy Load Embedded Content
import { Suspense, lazy } from 'react'
import { documentToReactComponents } from '@contentful/rich-text-react-renderer'
const EmbeddedVideo = lazy(() => import('./EmbeddedVideo'))
const renderOptions = {
renderNode: {
[BLOCKS.EMBEDDED_ENTRY]: (node) => {
return (
<Suspense fallback={<div>Loading...</div>}>
<EmbeddedVideo entry={node.data.target} />
</Suspense>
)
},
},
}
5. Web Fonts Loading
Custom fonts from Contentful can delay LCP.
Problem: Waiting for font files to load.
Diagnosis:
- Check if text is LCP element
- Look for font-related delays in waterfall
- Check for FOIT (Flash of Invisible Text)
Solutions:
A. Preload Fonts
// next.config.js or app/layout.tsx
export default function RootLayout() {
return (
<html>
<head>
<link
rel="preload"
href="/fonts/custom-font.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
</head>
<body>{children}</body>
</html>
)
}
B. Use font-display: swap
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom-font.woff2') format('woff2');
font-display: swap; /* Show fallback immediately */
font-weight: 400;
font-style: normal;
}
6. Framework-Specific Issues
Next.js Issues
Hydration Delays:
// Avoid large client-side components
'use client'
// Instead, use server components
async function ServerContent() {
const data = await getContentfulData()
return <div>{data}</div>
}
Bundle Size:
// next.config.js
module.exports = {
experimental: {
optimizeCss: true,
},
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
}
Gatsby Issues
Build Performance:
// gatsby-config.js
module.exports = {
plugins: [
{
resolve: 'gatsby-source-contentful',
options: {
spaceId: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
downloadLocal: true, // Download images locally
forceFullSync: false, // Only fetch changes
},
},
],
}
Framework-Specific Optimizations
Next.js Optimizations
Image Optimization:
import Image from 'next/image'
const contentfulLoader = ({ src, width, quality }) => {
const url = new URL(src)
url.searchParams.set('w', width.toString())
url.searchParams.set('q', (quality || 75).toString())
url.searchParams.set('fm', 'webp')
return url.toString()
}
export function ContentfulImage({ src, alt, ...props }) {
return (
<Image
loader={contentfulLoader}
src={src}
alt={alt}
{...props}
/>
)
}
Incremental Static Regeneration:
export async function getStaticProps() {
const data = await getContentfulData()
return {
props: { data },
revalidate: 60, // Rebuild every 60 seconds
}
}
Gatsby Optimizations
Deferred Static Generation:
// gatsby-config.js
module.exports = {
flags: {
FAST_DEV: true,
DEV_SSR: false,
PRESERVE_FILE_DOWNLOAD_CACHE: true,
},
}
Partial Hydration:
// Use gatsby-plugin-image for Contentful images
import { GatsbyImage } from 'gatsby-plugin-image'
export function ContentfulImage({ image }) {
return (
<GatsbyImage
image={image.gatsbyImageData}
alt={image.title}
loading="eager" // For LCP image
/>
)
}
Testing & Monitoring
Test LCP
Tools:
- PageSpeed Insights - Lab and field data
- WebPageTest - Detailed waterfall
- Chrome DevTools - Local testing
- Lighthouse CI - Automated testing
Test Different Pages:
- Homepage
- Blog posts
- Product pages (if e-commerce)
- Dynamic content pages
Test Different Devices:
- Desktop
- Mobile (Slow 3G, Fast 3G, 4G)
- Tablet
Monitor LCP Over Time
Real User Monitoring:
// Track LCP with Web Vitals
import { getLCP } from 'web-vitals'
getLCP((metric) => {
// Send to analytics
if (window.gtag) {
window.gtag('event', 'web_vitals', {
event_category: 'Web Vitals',
event_label: 'LCP',
value: Math.round(metric.value),
non_interaction: true,
})
}
})
Chrome User Experience Report (CrUX):
- Real user data in PageSpeed Insights
- Track trends over 28 days
Quick Wins Checklist
Priority optimizations for immediate LCP improvements:
- Use Contentful Image API with width/quality parameters
- Add explicit width/height to all Contentful images
- Set
loading="eager"on LCP image - Preload LCP image in
<head> - Use SSG or ISR instead of CSR for Contentful content
- Implement response caching for Contentful API
- Use GraphQL instead of REST API
- Optimize Rich Text processing (server-side if possible)
- Preload critical fonts
- Use Next.js Image component or gatsby-plugin-image
- Enable WebP format via Contentful Image API
- Test with PageSpeed Insights
Advanced Optimizations
Edge Caching
Cache Contentful content at the edge:
// Cloudflare Worker example
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const cache = caches.default
let response = await cache.match(request)
if (!response) {
response = await fetch(request)
// Cache Contentful API responses for 1 hour
if (request.url.includes('cdn.contentful.com')) {
const newResponse = new Response(response.body, response)
newResponse.headers.set('Cache-Control', 'max-age=3600')
event.waitUntil(cache.put(request, newResponse.clone()))
}
}
return response
}
Service Worker Caching
// service-worker.js
self.addEventListener('fetch', event => {
if (event.request.url.includes('images.ctfassets.net')) {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request).then(fetchResponse => {
return caches.open('contentful-images').then(cache => {
cache.put(event.request, fetchResponse.clone())
return fetchResponse
})
})
})
)
}
})
When to Hire a Developer
Consider hiring a developer if:
- LCP consistently over 4 seconds after optimizations
- Complex Contentful implementation requires refactoring
- Need advanced caching strategies
- Framework-specific expertise required
- Migration to different framework needed
Next Steps
For general LCP optimization strategies, see LCP Optimization Guide.