CLS Astro 4+

Fix CLS in Astro

Cumulative Layout Shift (CLS) is a critical Core Web Vital that measures how quickly the main content of your page becomes visible. In Astro applications, common issues include unoptimized images, render-blocking resources, and inefficient data loading patterns that delay the largest visible element.

This guide walks through five targeted fixes for CLS in Astro, with real code examples and before/after performance comparisons. Each step addresses a specific bottleneck in the Astro rendering pipeline.

Expected results

Following all five steps typically produces these improvements:

Before

0.18

CLS score (Needs Improvement) -- images without dimensions, font swap shifts, island hydration layout changes

After

0.02

CLS score (Good) -- all images sized, font fallback adjusted, stable island placeholders

Step-by-step fix

Always specify image dimensions with astro:assets

The most common CLS source in any framework is images loading without reserved space. Astro's <Image /> component automatically adds width and height attributes, but you must provide the source image dimensions. For remote images, always specify width and height props explicitly.

Common mistake: If you use <img> tags directly instead of the Astro Image component, you must manually add width and height attributes, or use CSS aspect-ratio to reserve space.
Astro -- Before (Bad)
---
// Missing dimensions causes layout shift
---
Photo


Banner
Astro -- After (Good)
---
import { Image } from 'astro:assets';
import banner from '../assets/banner.jpg'; // Dimensions auto-detected
---


Banner


Photo

Prevent font swap layout shifts with size-adjusted fallbacks

When custom fonts load with font-display: swap, the fallback font is replaced, often causing text to reflow. Use CSS size-adjust, ascent-override, and descent-override on your fallback font face to match the metrics of your custom font. Tools like Fallback Font Generator calculate these values automatically.

CSS -- Size-adjusted fallback
/* Match fallback metrics to custom font */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var-latin.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap;
}

/* Adjusted fallback prevents CLS during font swap */
@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  ascent-override: 90.49%;
  descent-override: 22.56%;
  line-gap-override: 0%;
  size-adjust: 107.64%;
}

body {
  font-family: 'Inter', 'Inter Fallback', sans-serif;
}

Reserve space for hydrating islands

When Astro islands hydrate (components with client:visible or client:idle), they can cause layout shift if the hydrated component renders at a different size than the server-rendered HTML. Ensure your island wrapper has fixed dimensions or uses CSS min-height to reserve space before hydration completes.

Astro -- Stable island wrapper
---
import SearchWidget from '../components/SearchWidget';
---


Stabilize dynamic content with CSS contain and aspect-ratio

For elements that load asynchronously (ads, embeds, dynamically injected content), use CSS aspect-ratio and contain: layout to prevent shifts. Define explicit containers for any content that loads after the initial render.

CSS -- Stable containers
/* Video embed container */
.video-embed {
  aspect-ratio: 16 / 9;
  width: 100%;
  background: var(--color-surface-2);
  contain: layout;
}

/* Ad slot with reserved space */
.ad-slot {
  min-height: 250px;
  width: 300px;
  contain: layout style;
  background: var(--color-surface-2);
}

/* Dynamic content area */
.dynamic-content {
  min-height: 200px;
  contain: layout;
  /* Smooth transitions instead of sudden shifts */
  transition: min-height 0.2s ease;
}

Handle view transitions without layout shift

Astro's View Transitions can cause CLS if elements move position between pages. Use transition:name to create smooth morph animations between shared elements, and avoid transition:animate='slide' on content that changes size between pages. For the safest CLS scores, use transition:animate='fade' as the default.

Astro -- Stable view transitions
---
import { ViewTransitions } from 'astro:transitions';
---


  
  
  
  


  
  
Hero

Quick checklist

  • All images use <Image /> from astro:assets with dimensions
  • Remote images have explicit width and height props
  • Font fallbacks use size-adjust and metric overrides
  • Island wrappers have min-height to reserve space
  • Dynamic content uses aspect-ratio and contain: layout
  • View transitions use fade or morph (not slide)
  • No unsized <img>, <video>, or <iframe> elements

Frequently asked questions

Astro sites typically achieve CLS scores under 0.05 because they generate static HTML with no hydration-related layout shifts in the main content. The most common CLS issues in Astro come from images without dimensions, font loading, and island hydration. With the optimizations in this guide, CLS under 0.02 is achievable.

Islands can cause CLS if the hydrated component renders at a different size than the server-rendered HTML placeholder. This is most common with client:visible components that expand when they hydrate. Use min-height on the wrapper element and CSS contain to isolate the island's layout impact. Components with client:idle typically hydrate before the user notices any shift.

View transitions can cause CLS during page navigation if elements change position or size between pages. Use transition:animate='fade' as a safe default, and transition:name on shared elements (headers, navigation) to create smooth morph animations. Avoid 'slide' animations on content areas where the incoming page has different dimensions.

Yes, when using the Image component from astro:assets with local images. Astro reads the image file at build time and automatically sets width and height. For remote images, you must provide dimensions explicitly via the width and height props. Without these, the browser cannot reserve space and layout shift will occur.

Use Chrome DevTools Performance panel to record a page load, then look for Layout Shift entries in the Experience row. The CLS Debugger extension highlights shifted elements in red. For Astro-specific issues, check if shifts correlate with island hydration by temporarily removing client:* directives. Use the Web Vitals Chrome extension for real-time CLS monitoring.

Set up real-user monitoring using the web-vitals JavaScript library (1.5KB). Send CLS data to your analytics platform (Google Analytics 4, custom endpoint). The attribution build identifies exactly which element caused each layout shift. For Astro, also monitor CLS after route transitions, as client-side navigation can trigger additional shifts not captured in initial page load.

Continue learning