Largest Contentful Paint (LCP) measures how quickly the main content of your DatoCMS-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.
DatoCMS-Specific LCP Issues
1. Unoptimized Imgix Images
The most common LCP issue is large, unoptimized images from DatoCMS's Imgix CDN.
Problem: Hero images or featured images loading at full resolution.
Diagnosis:
- Run PageSpeed Insights
- Check if DatoCMS image is the LCP element
- Look for "Properly size images" warning
Solutions:
A. Use DatoCMS Responsive Images
DatoCMS provides built-in Imgix image optimization:
// GraphQL query with responsive image
const BLOG_POST_QUERY = `
query BlogPost($slug: String!) {
blogPost(filter: { slug: { eq: $slug } }) {
id
title
coverImage {
responsiveImage(
imgixParams: {
fm: webp
fit: crop
w: 1920
h: 1080
auto: format
}
sizes: "(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 1200px"
) {
src
srcSet
webpSrcSet
sizes
width
height
aspectRatio
alt
title
bgColor
base64
}
}
}
}
`
B. Implement Responsive Images with Next.js
Use Next.js Image component with DatoCMS:
import Image from 'next/image'
export function DatoCMSImage({ responsiveImage }) {
return (
<Image
src={responsiveImage.src}
alt={responsiveImage.alt || ''}
width={responsiveImage.width}
height={responsiveImage.height}
priority={true} // For LCP image
quality={80}
placeholder="blur"
blurDataURL={responsiveImage.base64}
sizes={responsiveImage.sizes}
/>
)
}
C. Use next-datocms Image Component
Install and use the official DatoCMS Next.js package:
npm install react-datocms next-datocms
import { Image } from 'react-datocms'
export function BlogHero({ coverImage }) {
return (
<Image
data={coverImage.responsiveImage}
priority // For above-fold LCP images
lazyLoad={false}
pictureStyle={{ width: '100%', height: 'auto' }}
/>
)
}
D. Optimize Imgix Parameters
Manually optimize Imgix URLs for best performance:
export function getOptimizedImageUrl(
url: string,
width: number = 1200,
quality: number = 80
): string {
const params = new URLSearchParams({
auto: 'format,compress',
fit: 'crop',
w: width.toString(),
q: quality.toString(),
})
return `${url}?${params.toString()}`
}
// Usage
<img
src={getOptimizedImageUrl(post.coverImage.url, 1920, 80)}
alt={post.coverImage.alt}
width={1920}
height={1080}
loading="eager" // For LCP image
/>
2. Slow GraphQL Query Performance
Waiting for DatoCMS queries on the client causes LCP delays.
Problem: Content fetched on client-side, blocking render.
Diagnosis:
- Check Network tab for DatoCMS API calls
- Measure time to first byte (TTFB)
- Look for GraphQL queries blocking render
Solutions:
A. Use Static Site Generation (SSG)
Pre-render pages at build time:
Next.js:
// pages/blog/[slug].tsx
import { request } from '@/lib/datocms'
export async function getStaticProps({ params }) {
const query = `
query BlogPost($slug: String!) {
blogPost(filter: { slug: { eq: $slug } }) {
id
title
content
coverImage {
responsiveImage(imgixParams: { w: 1200, auto: format }) {
src
srcSet
width
height
base64
}
}
}
}
`
const { data } = await request({
query,
variables: { slug: params.slug },
})
return {
props: { post: data.blogPost },
revalidate: 3600, // ISR: Rebuild every hour
}
}
export async function getStaticPaths() {
const query = `{ allBlogPosts { slug } }`
const { data } = await request({ query })
return {
paths: data.allBlogPosts.map(post => ({
params: { slug: post.slug }
})),
fallback: 'blocking',
}
}
// gatsby-node.js
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
const result = await graphql(`
query {
allDatoCmsBlogPost {
nodes {
slug
}
}
}
`)
result.data.allDatoCmsBlogPost.nodes.forEach(post => {
createPage({
path: `/blog/${post.slug}`,
component: require.resolve('./src/templates/blog-post.tsx'),
context: { slug: post.slug },
})
})
}
B. Optimize GraphQL Queries
Fetch only necessary fields:
# Bad - fetches everything
query {
allBlogPosts {
_allReferencingPosts
_seoMetaTags
}
}
# Good - only needed fields
query {
allBlogPosts {
id
title
slug
coverImage {
responsiveImage(imgixParams: { w: 800, fit: crop, auto: format }) {
src
srcSet
width
height
base64
}
}
excerpt
}
}
C. Use DatoCMS CDN
Enable CDN for faster response times:
import { buildClient } from '@datocms/cma-client-browser'
export const client = buildClient({
apiToken: process.env.DATOCMS_API_TOKEN!,
environment: process.env.DATOCMS_ENVIRONMENT,
})
// For read-only queries, use Content Delivery API
export async function fetchDatoCMS(query: string) {
const response = await fetch('https://graphql.datocms.com/', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.DATOCMS_API_TOKEN}`,
'Content-Type': 'application/json',
'X-Environment': process.env.DATOCMS_ENVIRONMENT || 'main',
},
body: JSON.stringify({ query }),
next: { revalidate: 3600 }, // Next.js cache
})
return response.json()
}
D. Implement Query Caching
Cache DatoCMS responses:
// Simple in-memory cache
const queryCache = new Map<string, { data: any; timestamp: number }>()
const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
export async function cachedFetch(query: string) {
const cacheKey = query
const cached = queryCache.get(cacheKey)
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data
}
const data = await fetchDatoCMS(query)
queryCache.set(cacheKey, { data, timestamp: Date.now() })
return data
}
3. Client-Side Rendering (CSR) Delays
CSR requires additional round trips before rendering content.
Problem: Page waits for JavaScript, then fetches DatoCMS 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 query = `
query BlogPost($slug: String!) {
blogPost(filter: { slug: { eq: $slug } }) {
id
title
content
}
}
`
const { data } = await request({ query, variables: { slug: params.slug } })
return {
props: { post: data.blogPost },
}
}
B. Implement Streaming SSR (Next.js App Router)
// app/blog/[slug]/page.tsx
import { Suspense } from 'react'
async function BlogContent({ slug }) {
const { data } = await fetchDatoCMS(`
query { blogPost(filter: { slug: { eq: "${slug}" } }) { title, content } }
`)
return <article>{/* Content */}</article>
}
export default function BlogPage({ params }) {
return (
<Suspense fallback={<LoadingSkeleton />}>
<BlogContent slug={params.slug} />
</Suspense>
)
}
4. Large Modular Content
Processing large modular content blocks on the client is slow.
Problem: Modular content blocks rendering blocks paint.
Diagnosis:
- Check if LCP element is in modular content
- Profile content processing time
- Look for multiple embedded blocks
Solutions:
A. Process Modular Content on Server
// Server-side (Next.js)
import { renderMetaTags, renderNodeRule, StructuredText } from 'react-datocms'
export async function getStaticProps() {
const query = `
query {
blogPost {
content {
value
blocks {
__typename
... on ImageBlockRecord {
id
image {
responsiveImage(imgixParams: { w: 800 }) {
src
srcSet
width
height
}
}
}
}
}
}
}
`
const { data } = await request({ query })
return {
props: { post: data.blogPost },
}
}
B. Lazy Load Embedded Content
import { Suspense, lazy } from 'react'
import { StructuredText } from 'react-datocms'
const VideoBlock = lazy(() => import('./VideoBlock'))
export function ModularContent({ content }) {
return (
<StructuredText
data={content}
renderBlock={({ record }) => {
if (record.__typename === 'VideoBlockRecord') {
return (
<Suspense fallback={<div>Loading video...</div>}>
<VideoBlock data={record} />
</Suspense>
)
}
return null
}}
/>
)
}
5. Web Fonts Loading
Custom fonts can delay LCP.
Problem: Waiting for font files to load.
Solutions:
A. Preload Fonts
// app/layout.tsx or next.config.js
export default function RootLayout({ children }) {
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 */
}
Testing & Monitoring
Test LCP
Tools:
- PageSpeed Insights - Lab and field data
- WebPageTest - Detailed waterfall
- Chrome DevTools - Local testing
- Lighthouse CI - Automated testing
Monitor LCP Over Time
// Track LCP with Web Vitals
import { onLCP } from 'web-vitals'
onLCP((metric) => {
if (window.gtag) {
window.gtag('event', 'web_vitals', {
event_category: 'Web Vitals',
event_label: 'LCP',
value: Math.round(metric.value),
non_interaction: true,
})
}
})
Quick Wins Checklist
- Use DatoCMS responsiveImage with Imgix params
- Add explicit width/height to all images
- Set
priority={true}on LCP image - Preload LCP image in
<head> - Use SSG or ISR instead of CSR
- Optimize GraphQL queries (fetch only needed fields)
- Use DatoCMS CDN
- Implement query caching
- Process modular content server-side
- Preload critical fonts
- Use Next.js Image component or Gatsby plugin
- Enable WebP via Imgix
- Test with PageSpeed Insights
Next Steps
For general LCP optimization strategies, see LCP Optimization Guide.