General Guide: See Global CLS Guide for universal concepts and fixes.
What is CLS?
Cumulative Layout Shift measures visual stability. Google recommends CLS under 0.1. ButterCMS is headless, so CLS originates entirely from your frontend implementation -- how you handle async API data, image rendering, and component loading.
ButterCMS-Specific CLS Causes
- Content popping in after API fetch -- client-side rendering shows a loading state then replaces it with API content, shifting the entire page layout
- Images without dimensions -- ButterCMS image fields do not include width/height metadata in API responses by default
- Blog post content variability -- posts with different numbers of images, embeds, and content lengths cause variable-height containers
- Page builder components -- ButterCMS's Page Type components render at unknown heights until their content loads
- Third-party embeds in rich text -- YouTube videos, tweets, and other embeds in ButterCMS content fields render without reserved space
Fixes
1. Use SSG/SSR to Eliminate Loading State Shifts
The primary CLS fix for headless CMS: render content server-side so there is no loading-to-content swap:
// CAUSES CLS: Client-side fetch with loading state
function BlogPost({ slug }) {
const [post, setPost] = useState(null);
useEffect(() => {
butter.post.retrieve(slug).then(resp => setPost(resp.data.data));
}, [slug]);
if (!post) return <Skeleton />; // Layout shift when replaced
return <Article post={post} />;
}
// NO CLS: Server-side fetch (Next.js)
export async function getStaticProps({ params }) {
const resp = await butter.post.retrieve(params.slug);
return { props: { post: resp.data.data }, revalidate: 60 };
}
function BlogPost({ post }) {
return <Article post={post} />; // Content rendered immediately
}
2. Set Image Dimensions from ButterCMS Fields
ButterCMS image URLs go through Filestack CDN. Extract dimensions or set known aspect ratios:
// Helper to add dimensions to ButterCMS images
function ButterImage({ url, alt, width = 800, aspectRatio = '16/9', eager = false }) {
if (!url) return null;
const optimizedUrl = `${url}?w=${width}&q=80&auto=format`;
return (
<div style={{ aspectRatio, overflow: 'hidden', background: '#f0f0f0' }}>
<img
src={optimizedUrl}
alt={alt || ''}
width={width}
height={Math.round(width / (16/9))}
loading={eager ? 'eager' : 'lazy'}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</div>
);
}
// Usage in blog post template
function FeaturedImage({ post }) {
return (
<ButterImage
url={post.featured_image}
alt={post.featured_image_alt}
width={1200}
aspectRatio="16/9"
eager={true}
/>
);
}
3. Stabilize Page Builder Component Layouts
ButterCMS Page Types define components with different field sets. Set minimum heights per component type:
// Component renderer with CLS prevention
const COMPONENT_MIN_HEIGHTS = {
hero_section: 500,
feature_grid: 400,
testimonial_slider: 300,
cta_banner: 200,
content_block: 150,
};
function PageComponent({ component, type }) {
const minHeight = COMPONENT_MIN_HEIGHTS[type] || 100;
return (
<section
style={{
minHeight: `${minHeight}px`,
contain: 'layout',
}}
>
<ComponentRenderer component={component} type={type} />
</section>
);
}
4. Handle Rich Text Embeds
ButterCMS's rich text editor allows embedding videos and third-party content. Process the HTML to add dimensions:
// Post-process ButterCMS HTML content to add embed dimensions
function processButterContent(html) {
if (!html) return '';
// Add aspect ratio to iframes (YouTube, Vimeo)
html = html.replace(
/<iframe([^>]*)>/g,
'<div style="aspect-ratio:16/9;contain:layout;"><iframe$1 style="width:100%;height:100%;border:0;">'
);
html = html.replace(/<\/iframe>/g, '</iframe></div>');
// Add dimensions to images without them
html = html.replace(
/<img(?![^>]*width)([^>]*)>/g,
'<img$1 style="aspect-ratio:16/9;width:100%;height:auto;object-fit:cover;">'
);
return html;
}
// Use in your post component
function PostBody({ content }) {
return (
<div
className="post-content"
dangerouslySetInnerHTML={{ __html: processButterContent(content) }}
/>
);
}
5. Preload Fonts in Your Frontend
<!-- In your document head -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preload" href="/fonts/body-font.woff2" as="font" type="font/woff2" crossorigin />
@font-face {
font-family: 'BodyFont';
src: url('/fonts/body-font.woff2') format('woff2');
font-display: swap;
size-adjust: 104%;
}
Measuring CLS on ButterCMS
- Chrome DevTools Performance tab -- record page load and look for layout-shift entries. Common culprits are the transition from loading skeleton to real content.
- Compare CSR vs. SSR -- if you see CLS from content loading in, switching to SSR/SSG will eliminate it
- Test blog posts with different content -- posts with many images, embeds, or variable-length content expose CLS issues that shorter posts hide
- Web Vitals monitoring -- add real-user monitoring since lab tests may not catch CLS from lazy-loaded content:
// Track CLS in production
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
// Send to analytics
gtag('event', 'cls', { value: entry.value });
}
}
}).observe({ type: 'layout-shift', buffered: true });
Analytics Script Impact
Since ButterCMS is headless, analytics CLS depends on your frontend framework:
- Next.js -- use
next/scriptwithstrategy="afterInteractive"to avoid injecting scripts that shift content - Consent managers -- OneTrust, Cookiebot banners must use
position: fixedoverlay; do not push page content down - A/B testing tools -- Optimizely, VWO modify DOM and cause CLS; prefer server-side testing with ButterCMS's API-based content variants