Fix CLS Issues on Graphcms (Layout Shift) | OpsBlu Docs

Fix CLS Issues on Graphcms (Layout Shift)

Stabilize GraphCMS sites by using asset dimension metadata from the GraphQL API, reserving rich text space, and preloading fonts.

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 width and height in 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

  1. Chrome DevTools Performance tab -- look for shifts during hydration and content loading
  2. Test with different content -- pages with many images and rich text embeds expose more CLS
  3. Compare SSG vs CSR -- SSG eliminates loading-state CLS entirely
  4. Web Vitals RUM for production monitoring