Fix LCP Issues on Datocms (Loading Speed) | OpsBlu Docs

Fix LCP Issues on Datocms (Loading Speed)

Speed up DatoCMS LCP with Imgix responsive image parameters, leaner GraphQL queries, and static site generation on your frontend.

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:

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:

// 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:

  1. PageSpeed Insights - Lab and field data
  2. WebPageTest - Detailed waterfall
  3. Chrome DevTools - Local testing
  4. 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.