CLS Nuxt 3+

Fix CLS in Nuxt

Cumulative Layout Shift (CLS) is a critical Core Web Vital that measures how quickly the main content of your page becomes visible. In Nuxt 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 Nuxt, with real code examples and before/after performance comparisons. Each step addresses a specific bottleneck in the Nuxt rendering pipeline.

Expected results

Following all five steps typically produces these improvements:

Before

0.25

CLS score (Needs Improvement) -- images without dimensions, font swap shifts, hydration mismatches, dynamic content shifts

After

0.03

CLS score (Good) -- NuxtImage with dimensions, optimized fonts, stable hydration, contained dynamic content

Step-by-step fix

Use NuxtImage with explicit dimensions

The <NuxtImg> and <NuxtPicture> components from @nuxt/image automatically generate width and height attributes when you provide dimensions. This ensures the browser reserves space for images before they load, eliminating the most common CLS source.

Common mistake: Always specify width and height props on NuxtImg/NuxtPicture components, especially for remote images. Without dimensions, the browser cannot calculate aspect ratio to reserve space before the image loads.
Vue -- Before (Bad)
Vue -- After (Good)

Prevent font-induced layout shifts with @nuxt/fonts

The @nuxt/fonts module automatically handles font optimization, but you should also configure size-adjusted fallbacks for custom fonts. The module downloads fonts at build time, applies font-display: swap, and generates optimal CSS. For additional CLS protection, define metric-adjusted fallback fonts.

CSS -- assets/css/fonts.css
/* @nuxt/fonts handles the primary font automatically */
/* Add metric-adjusted fallbacks for extra CLS protection */

@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 {
  /* @nuxt/fonts injects 'Inter' automatically */
  /* Add fallback chain for CLS protection */
  font-family: 'Inter', 'Inter Fallback', system-ui, sans-serif;
}

Fix hydration mismatches that cause layout shifts

When the server-rendered HTML differs from the client-rendered DOM, Vue 3 performs a hydration mismatch correction that can cause visible layout shifts. Common causes: using Date.now() or Math.random() in templates, browser-specific rendering, and conditional content based on window/document. Use <ClientOnly> for browser-specific content.

Vue -- Handling hydration-sensitive content

Reserve space for dynamic and async content

Components that load data asynchronously (via useFetch or useAsyncData) can cause CLS when the data arrives and the content expands. Use skeleton placeholders with matching dimensions, CSS min-height, and aspect-ratio to reserve space while data loads.

Vue -- Stable async content


Stabilize v-if/v-show transitions and page transitions

Vue's v-if adds/removes elements from the DOM, potentially shifting surrounding content. For content that toggles visibility, prefer v-show (which uses CSS display:none) to keep the element in the layout flow. For page transitions, use transform-based animations that do not affect layout.

Vue -- Stable transitions


Quick checklist

  • All images use <NuxtImg> or <NuxtPicture> with dimensions
  • @nuxt/fonts installed with size-adjusted CSS fallbacks
  • Browser-only content wrapped in <ClientOnly> with sized fallback
  • Async content has skeleton placeholders with min-height
  • Page transitions use opacity only (no height/position changes)
  • v-show preferred over v-if for toggle visibility
  • No Date.now(), Math.random(), or window in SSR templates

Frequently asked questions

A well-optimized Nuxt 3 site should achieve CLS under 0.05. With NuxtImage dimensions, @nuxt/fonts optimization, proper ClientOnly wrappers, and skeleton loading states, CLS under 0.03 is achievable. The most common CLS sources in Nuxt are image shifts, font loading, and hydration mismatches.

Yes. When the server-rendered HTML differs from client-rendered DOM, Vue 3 corrects the mismatch by updating the DOM, which can cause visible layout shifts. Common causes include using browser APIs (window, document) in setup(), Date/Math.random in templates, and conditional rendering based on client state. Use ClientOnly with dimension-matching fallback placeholders.

The @nuxt/fonts module downloads fonts at build time and self-hosts them, eliminating external requests. It automatically applies font-display: swap and generates preload links. This reduces the time between fallback font display and custom font rendering, minimizing the text reflow that causes CLS. For additional protection, add size-adjusted CSS fallbacks.

Use v-show when toggling visibility of content that should maintain its space in the layout. v-show uses CSS display:none, keeping the element in the DOM but hidden. Use v-if with a fixed-height container when you need to conditionally render content that should not occupy space when absent. The key is ensuring the container dimensions stay stable regardless of content state.

Use the Web Vitals Chrome extension for real-time CLS scores, then open Chrome DevTools Performance panel to record a page load. Look for Layout Shift entries in the Experience track. For hydration mismatches, check the Vue DevTools Hydration Mismatch tab. Enable Nuxt's debug mode in development to see hydration warnings in the console.

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 Nuxt, also monitor CLS after route transitions, as client-side navigation can trigger additional shifts not captured in initial page load.

Continue learning