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.,
useEffectin 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
- Separate API time from render time -- use Chrome DevTools Network tab to see ButterCMS API response times (should be under 200ms from CDN)
- Compare SSG vs. SSR vs. CSR -- test the same page with each rendering strategy to quantify the difference
- ButterCMS Dashboard -- check API usage and response times under Settings > API
- 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-- usestrategy="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/deferattributes
// Next.js analytics loading pattern
import Script from 'next/script';
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXX"
strategy="afterInteractive"
/>