Fix CLS in SvelteKit
Cumulative Layout Shift (CLS) is a critical Core Web Vital that measures how quickly the main content of your page becomes visible. In Svelte 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 Svelte, with real code examples and before/after performance comparisons. Each step addresses a specific bottleneck in the Svelte rendering pipeline.
Expected results
Following all five steps typically produces these improvements:
Before
0.22
CLS score (Needs Improvement) -- unsized images, font swap shifts, transition animations causing reflow
After
0.03
CLS score (Good) -- all images sized, adjusted font fallbacks, stable transitions and dynamic content
Step-by-step fix
Use enhanced:img for automatic dimension detection
SvelteKit's enhanced:img preprocessor reads image dimensions at build time and automatically adds width and height attributes. This ensures the browser reserves the correct space before the image loads, eliminating the most common CLS source.

Prevent font-induced layout shifts
When web fonts load with font-display: swap, the fallback font is replaced, often causing text to reflow. Create a size-adjusted fallback that matches your custom font's metrics to minimize or eliminate the shift.
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900;
font-display: 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', system-ui, sans-serif;
}
Stabilize Svelte transitions and animations
Svelte's built-in transition: and animate: directives modify element dimensions during animation, potentially causing CLS. For transitions that trigger during page load (not from user interaction), ensure they do not change element height or position. Use opacity and transform only -- these are compositor-layer properties that never cause layout shifts.
{#if visible}
Content
{/if}
{#if visible}
Sliding content
{/if}
Reserve space for dynamic and conditional content
Svelte's {#if} and {#each} blocks can cause CLS when content appears or changes after initial render. Reserve space with min-height or aspect-ratio for containers that hold dynamic content. For loading states, use skeleton placeholders that match the final content dimensions.
{#if data}
{#each data as product}
{product.name}
{/each}
{:else}
{#each Array(6) as _}
{/each}
{/if}
Use CSS contain to isolate layout changes
The CSS contain property tells the browser that an element's internals do not affect the rest of the page layout. Apply contain: layout style to sections that update dynamically (ads, widgets, live data) to prevent their changes from shifting surrounding content.
/* Isolate dynamic sections */
.sidebar-widget {
contain: layout style;
min-height: 300px;
}
.ad-slot {
contain: layout style;
aspect-ratio: 300 / 250;
}
/* Isolate each list item so additions don't shift others */
.notification-list {
contain: layout;
}
.notification-item {
contain: layout style;
min-height: 64px;
}
Quick checklist
-
All images use
enhanced:imgor have explicitwidth/height -
Font fallbacks use
size-adjustand metric overrides -
Load-time transitions use only
opacityandtransform -
Dynamic content containers have
min-heightoraspect-ratio - Loading skeletons match final content dimensions
-
CSS
contain: layoutapplied to dynamic sections - No unsized embeds, iframes, or ad slots
Frequently asked questions
SvelteKit sites typically achieve CLS under 0.05 with basic optimizations. Because Svelte compiles to minimal DOM operations and SvelteKit supports SSR/SSG, there are fewer hydration-related layout shifts compared to React frameworks. With the optimizations in this guide, CLS under 0.03 is achievable.
Transitions triggered by user interaction (clicking a button, hovering) do not count toward CLS because they happen within 500ms of user input. However, transitions that run on page load (like intro animations on mount) can contribute to CLS if they change element position or size. Use opacity and transform-based transitions for load-time animations.
SvelteKit's navigation triggers the beforeNavigate and afterNavigate lifecycle hooks. During page transitions, the outgoing content is removed and incoming content is rendered. Use the onNavigate API with View Transitions for smooth cross-page animations. Ensure both pages have consistent header heights and navigation positions to avoid shift during transition.
SSR reduces CLS because the browser receives complete HTML with all content dimensions defined. Without SSR, a client-rendered page starts empty and content pops in as JavaScript runs, causing significant CLS. However, SSR alone does not prevent CLS from images without dimensions, font loading, or dynamic content injected after hydration.
Open Chrome DevTools, go to the Performance panel, and record a page load. Look for Layout Shift entries in the Experience track. Click each shift to see which elements moved. For Svelte-specific debugging, check if shifts correlate with component mount events by adding console.log in onMount callbacks. The Web Vitals Chrome extension provides real-time CLS scores.
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 Svelte, also monitor CLS after route transitions, as client-side navigation can trigger additional shifts not captured in initial page load.
Continue learning
Fix LCP in SvelteKit
Related performance optimization for the same framework.
GuideComplete CLS Guide
Deep dive into CLS -- thresholds, measurement, and optimization strategies.
FixFix CLS in React
Compare CLS fixes across different frameworks.
ToolCWV Score Explainer
Enter your scores for personalized fix recommendations.