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

Fix LCP Issues on Buttercms (Loading Speed)

Improve ButterCMS LCP by optimizing API response payloads, using CDN-hosted image transforms, and prerendering headless frontend pages.

General Guide: See Global LCP Guide for universal concepts and fixes.

What is LCP?

Largest Contentful Paint measures when the largest content element becomes visible. Google recommends LCP under 2.5 seconds. ButterCMS is a headless CMS, so LCP depends on your frontend framework's rendering strategy, API response times, and how you handle ButterCMS image assets.

ButterCMS-Specific LCP Causes

  • Client-side API fetching -- if your frontend fetches ButterCMS content in the browser (e.g., useEffect in React), LCP waits for: JS download + parse + API call + render
  • Unoptimized ButterCMS images -- ButterCMS hosts images on its CDN but serves originals by default unless you use URL-based transforms
  • Large API payloads -- requesting full page content including all fields and nested references when only a few fields are needed
  • Frontend framework hydration -- React/Vue/Angular SPA hydration adds 500-2000ms before any content is interactive, delaying LCP
  • No static generation -- using client-side rendering instead of SSG/SSR for content that rarely changes

Fixes

1. Use SSG or SSR Instead of Client-Side Fetching

The biggest LCP improvement for any headless CMS. Fetch content at build time or server-side:

// Next.js with ButterCMS -- use getStaticProps for SSG
import Butter from 'buttercms';
const butter = Butter(process.env.BUTTERCMS_API_TOKEN);

export async function getStaticProps() {
  const resp = await butter.page.retrieve('*', 'homepage');
  return {
    props: { page: resp.data.data },
    revalidate: 60, // ISR: regenerate every 60 seconds
  };
}

// For dynamic pages like blog posts
export async function getStaticPaths() {
  const resp = await butter.post.list({ page_size: 100 });
  return {
    paths: resp.data.data.map((post) => ({
      params: { slug: post.slug },
    })),
    fallback: 'blocking', // SSR on first visit for new posts
  };
}

For non-Next.js frameworks (Gatsby, Nuxt, Astro), use their equivalent static data fetching.

2. Use ButterCMS Image Transforms

ButterCMS images are served via a CDN that supports URL-based transforms. Use these instead of serving originals:

// ButterCMS image URL transform helper
function optimizeButterImage(url, { width = 800, quality = 80, format = 'webp' } = {}) {
  if (!url) return '';
  // ButterCMS uses Filestack CDN for images
  // Append transform parameters
  const separator = url.includes('?') ? '&' : '?';
  return `${url}${separator}w=${width}&q=${quality}&auto=format`;
}

// In your React component
function HeroSection({ page }) {
  const heroImage = page.fields.hero_image;
  return (
    <img
      src={optimizeButterImage(heroImage, { width: 1920 })}
      srcSet={`
        ${optimizeButterImage(heroImage, { width: 640 })} 640w,
        ${optimizeButterImage(heroImage, { width: 1024 })} 1024w,
        ${optimizeButterImage(heroImage, { width: 1920 })} 1920w
      `}
      sizes="100vw"
      width="1920"
      height="600"
      alt={page.fields.hero_alt || ''}
      loading="eager"
      fetchPriority="high"
    />
  );
}

3. Reduce API Payload Size

ButterCMS API requests can return large payloads with nested content. Request only what you need:

// BAD: Fetch entire page with all fields
const resp = await butter.page.retrieve('*', 'homepage');

// GOOD: Fetch specific fields only
const resp = await butter.page.retrieve('*', 'homepage', {
  fields: 'hero_image,hero_title,hero_subtitle,seo_title,seo_description',
});

// For blog post listings -- don't fetch full body content
const resp = await butter.post.list({
  page_size: 10,
  exclude_body: true, // Skip full post content in listings
  fields: 'slug,title,featured_image,summary,published',
});

4. Preload LCP Image

Add preload hints in your document head:

// Next.js _document.js or layout component
import Head from 'next/head';

function PageHead({ heroImageUrl }) {
  return (
    <Head>
      {heroImageUrl && (
        <>
          <link rel="preconnect" href="https://cdn.buttercms.com" />
          <link
            rel="preload"
            as="image"
            href={optimizeButterImage(heroImageUrl, { width: 1920 })}
            type="image/webp"
          />
        </>
      )}
    </Head>
  );
}

5. Implement Stale-While-Revalidate Caching

For SSR deployments, cache API responses:

// Simple in-memory cache for ButterCMS API responses
const cache = new Map();
const CACHE_TTL = 300000; // 5 minutes

async function getCachedPage(slug) {
  const cached = cache.get(slug);
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return cached.data;
  }

  const resp = await butter.page.retrieve('*', slug);
  cache.set(slug, { data: resp.data.data, timestamp: Date.now() });
  return resp.data.data;
}

Measuring LCP on ButterCMS

  1. Separate API time from render time -- use Chrome DevTools Network tab to see ButterCMS API response times (should be under 200ms from CDN)
  2. Compare SSG vs. SSR vs. CSR -- test the same page with each rendering strategy to quantify the difference
  3. ButterCMS Dashboard -- check API usage and response times under Settings > API
  4. Key pages to test: homepage (typically the heaviest with multiple page fields), blog listing (many image thumbnails), individual blog posts (featured image is usually LCP)

Analytics Script Impact

Since ButterCMS is headless, analytics integration depends on your frontend:

  • Next.js with next/script -- use strategy="afterInteractive" for analytics to avoid blocking LCP
  • React SPA -- load analytics after hydration, not in the initial bundle
  • Static sites -- place analytics scripts with async/defer attributes
// Next.js analytics loading pattern
import Script from 'next/script';

<Script
  src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXX"
  strategy="afterInteractive"
/>