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. Hygraph (formerly GraphCMS) is headless, so CLS comes from your frontend -- specifically how you handle async GraphQL data, image assets, and rich text rendering.
Hygraph-Specific CLS Causes
- Loading state to content swap -- client-side GraphQL fetching shows a loading state then replaces with data
- Assets without dimensions -- Hygraph assets include
widthandheightin the API but many implementations do not use them - Rich text AST rendering -- Hygraph's rich text field returns an AST; rendering it client-side can produce variable-height content
- Component field variability -- Hygraph's modular content (Union types) render different components at different heights
- Embedded assets in rich text -- images and videos referenced inline in rich text fields load without reserved space
Fixes
1. Use Asset Dimensions from GraphQL
Hygraph provides width and height on all assets. Always query and use them:
query {
post(where: { slug: "my-post" }) {
title
featuredImage {
url
width # Always query these
height # Always query these
}
}
}
function FeaturedImage({ image }) {
return (
<div style={{ aspectRatio: `${image.width} / ${image.height}` }}>
<img
src={`${image.url}?width=800&format=auto`}
width={image.width}
height={image.height}
alt=""
loading="lazy"
style={{ width: '100%', height: 'auto' }}
/>
</div>
);
}
2. Eliminate Loading State CLS with SSG
// Use getStaticProps to render content immediately
export async function getStaticProps({ params }) {
const { data } = await hygraphClient.query({ query: GET_POST, variables: { slug: params.slug } });
return { props: { post: data.post }, revalidate: 60 };
}
3. Handle Rich Text Embeds
Process Hygraph rich text AST to reserve space for embedded assets:
// Custom renderer for Hygraph rich text with CLS prevention
const renderers = {
Asset: {
image: ({ url, width, height, altText }) => (
<div style={{ aspectRatio: `${width} / ${height}`, contain: 'layout' }}>
<img src={`${url}?width=800&format=auto`} width={width} height={height}
alt={altText || ''} loading="lazy" style={{ width: '100%', height: 'auto' }} />
</div>
),
video: ({ url }) => (
<div style={{ aspectRatio: '16/9', contain: 'layout' }}>
<video src={url} controls style={{ width: '100%', height: '100%' }} />
</div>
),
},
embed: {
// External embeds (YouTube, etc.)
Node: ({ nodeId }) => (
<div style={{ aspectRatio: '16/9', contain: 'layout', minHeight: '300px' }}>
<iframe src={nodeId} style={{ width: '100%', height: '100%', border: 0 }} />
</div>
),
},
};
4. Stabilize Modular Content Components
Hygraph's Union types render different components. Set minimum heights:
const COMPONENT_HEIGHTS = {
Hero: 500, FeatureGrid: 400, Testimonial: 250, CallToAction: 200,
};
function ModularContent({ component }) {
const minHeight = COMPONENT_HEIGHTS[component.__typename] || 100;
return (
<section style={{ minHeight, contain: 'layout' }}>
<ComponentRenderer component={component} />
</section>
);
}
5. Preload Fonts
<link rel="preload" href="/fonts/body.woff2" as="font" type="font/woff2" crossorigin />
Measuring CLS on Hygraph
- Chrome DevTools Performance tab -- look for shifts during hydration and content loading
- Test with different content -- pages with many images and rich text embeds expose more CLS
- Compare SSG vs CSR -- SSG eliminates loading-state CLS entirely
- Web Vitals RUM for production monitoring