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

Fix CLS Issues on Keystonejs (Layout Shift)

Stabilize KeystoneJS layouts by reserving GraphQL query content containers, sizing image field outputs, and preloading custom 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. KeystoneJS is headless, so CLS comes from your frontend -- how you handle async GraphQL data loading, image rendering, and document field content.

KeystoneJS-Specific CLS Causes

  • Loading state swaps -- client-side GraphQL fetching shows skeleton/loading then replaces with data
  • Image fields without dimensions -- Keystone's image field includes width/height but implementations often skip them
  • Document field content variability -- Keystone's document field (structured content) renders at unknown heights
  • Relationship field content -- resolved relationships load additional content that changes layout
  • Admin UI scripts in production -- accidentally including Keystone's admin bundle in the frontend

Fixes

1. Use SSG to Eliminate Loading CLS

Server-side data fetching removes the loading-to-content transition entirely (see LCP guide for code example).

2. Use Image Dimensions from Keystone

Keystone's image field provides width and height. Always use them:

function KeystoneImage({ image, eager = false }) {
  if (!image) return null;
  return (
    <div style={{ aspectRatio: `${image.width} / ${image.height}` }}>
      <img
        src={image.url}
        width={image.width} height={image.height}
        alt={image.alt || ''}
        loading={eager ? 'eager' : 'lazy'}
        style={{ width: '100%', height: 'auto' }}
      />
    </div>
  );
}

3. Stabilize Document Field Rendering

Keystone's document field returns structured JSON. Reserve space during rendering:

function DocumentContent({ document }) {
  return (
    <div className="document-content" style={{ minHeight: '200px', contain: 'layout' }}>
      <DocumentRenderer document={document} />
    </div>
  );
}
/* Handle images/embeds within document content */
.document-content img { max-width: 100%; height: auto; aspect-ratio: attr(width) / attr(height); }
.document-content iframe { aspect-ratio: 16/9; width: 100%; height: auto; }

4. Reserve Space for Relationship Content

function RelatedPosts({ posts }) {
  return (
    <div style={{ minHeight: posts?.length ? `${posts.length * 120}px` : '120px', contain: 'layout' }}>
      {posts?.map(post => (
        <div key={post.id} style={{ minHeight: '100px' }}>
          <PostCard post={post} />
        </div>
      ))}
    </div>
  );
}

5. Preload Fonts

<link rel="preload" href="/fonts/body.woff2" as="font" type="font/woff2" crossorigin />

Measuring CLS

  1. Chrome DevTools Performance tab -- filter for layout-shift entries
  2. Compare SSG vs CSR -- SSG eliminates loading-state CLS
  3. Test pages with document fields containing mixed content types
  4. Web Vitals RUM for production monitoring