Nuxt Performance Optimization: Fix Core Web Vitals Issues

Nuxt is fast by default. Server-side rendering, automatic code splitting, and smart prefetching are all built in. But I've still seen Nuxt sites with poor Core Web Vitals, usually because of a few common mistakes.

This guide covers the Nuxt-specific optimizations that actually move the needle on LCP, CLS, and INP.

Images: Use NuxtImg

The @nuxt/image module is Nuxt's answer to optimized images. It handles lazy loading, responsive sizing, and modern formats. If you're using plain <img> tags, you're leaving performance on the table.

Install it:

npm install @nuxt/image

Configure it:

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/image'],
  image: {
    quality: 80,
    formats: ['webp', 'avif'],
  }
});

Use it:

<template>
  <!-- Bad -->
  <img src="/hero.jpg" alt="Hero" />
  
  <!-- Good -->
  <NuxtImg 
    src="/hero.jpg" 
    alt="Hero"
    width="1200"
    height="600"
    preload
  />
</template>

Key props:

  • preload - For LCP images. Adds a preload hint and disables lazy loading.
  • loading="lazy" - Default behavior, good for below-fold images.
  • sizes - Responsive sizing: sizes="(max-width: 768px) 100vw, 50vw"
  • format - Force a specific format: format="webp"

For background images, use NuxtPicture:

<NuxtPicture
  src="/hero.jpg"
  :imgAttrs="{ class: 'hero-bg' }"
/>

Fonts: Use @nuxtjs/fontaine or inline critical fonts

Font loading is a common source of CLS and slow LCP. Nuxt has a few options:

Option 1: Fontaine (automatic font fallback matching)

npm install @nuxtjs/fontaine
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/fontaine'],
});

Fontaine automatically creates a fallback font with matching metrics, eliminating layout shift when the real font loads.

Option 2: Preload critical fonts

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      link: [
        {
          rel: 'preload',
          href: '/fonts/inter.woff2',
          as: 'font',
          type: 'font/woff2',
          crossorigin: 'anonymous'
        }
      ]
    }
  }
});

Option 3: Google Fonts module

npm install @nuxtjs/google-fonts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/google-fonts'],
  googleFonts: {
    families: {
      Inter: [400, 600, 700],
    },
    display: 'swap',
    preload: true,
  }
});

Reduce JavaScript: Use ClientOnly wisely

Nuxt's <ClientOnly> wrapper is useful but often overused. Everything inside it ships JavaScript to the client and doesn't render on the server.

Bad - wrapping too much:

<ClientOnly>
  <div class="product-page">
    <ProductImage :src="product.image" />
    <ProductDetails :product="product" />
    <AddToCartButton :productId="product.id" />
  </div>
</ClientOnly>

Good - only wrap what needs client-side JS:

<div class="product-page">
  <ProductImage :src="product.image" />
  <ProductDetails :product="product" />
  <ClientOnly>
    <AddToCartButton :productId="product.id" />
  </ClientOnly>
</div>

Rule: Only use <ClientOnly> for components that genuinely need browser APIs (localStorage, window, etc.) or have heavy client-side interactivity.

Lazy load components

For heavy components below the fold, use defineAsyncComponent:

<script setup>
const HeavyChart = defineAsyncComponent(() => 
  import('~/components/HeavyChart.vue')
);
</script>

<template>
  <HeavyChart v-if="showChart" :data="chartData" />
</template>

Or use Nuxt's auto-import with the Lazy prefix:

<template>
  <!-- Automatically lazy-loaded -->
  <LazyHeavyChart :data="chartData" />
</template>

Any component prefixed with Lazy is automatically code-split and lazy-loaded.

Optimize data fetching

Nuxt offers multiple data fetching methods. Choose the right one:

useFetch - For most cases:

<script setup>
const { data: products } = await useFetch('/api/products');
</script>

Runs on server during SSR, caches the result, and hydrates on client.

useAsyncData - When you need more control:

<script setup>
const { data: product } = await useAsyncData(
  `product-${id}`,
  () => $fetch(`/api/products/${id}`)
);
</script>

$fetch - For client-only or event handlers:

<script setup>
async function loadMore() {
  const more = await $fetch('/api/products', { 
    params: { page: page.value + 1 } 
  });
  products.value.push(...more);
}
</script>

Avoid: fetching on mount with onMounted:

<script setup>
// Bad - runs after SSR, causes hydration delay
onMounted(async () => {
  const data = await fetch('/api/products').then(r => r.json());
});
</script>

Payload optimization

Large payloads slow down hydration. Keep your data minimal.

Bad - fetching everything:

<script setup>
// Returns all product fields including large descriptions, metadata, etc.
const { data } = await useFetch('/api/products');
</script>

Good - fetch only what you need:

<script setup>
const { data } = await useFetch('/api/products', {
  pick: ['id', 'name', 'price', 'thumbnail']
});
</script>

The pick option strips unused fields from the payload, reducing transfer size and hydration time.

Quick wins checklist

LCP

  • Use <NuxtImg> with preload for hero images
  • Preload critical fonts
  • Minimize blocking JavaScript

CLS

  • Always set width and height on images
  • Use Fontaine for automatic font fallback matching
  • Reserve space for dynamic content

INP

  • Minimize <ClientOnly> usage
  • Use Lazy prefix for below-fold components
  • Keep payloads small with pick

Debugging in Nuxt

Enable the Nuxt DevTools for performance insights:

// nuxt.config.ts
export default defineNuxtConfig({
  devtools: { enabled: true }
});

The DevTools show component render times, payload sizes, and more.

Frequently Asked Questions

Why is my Nuxt site slow when Nuxt is supposed to be fast?

Common issues: using plain <img> instead of <NuxtImg>, overusing <ClientOnly>, fetching data with onMounted instead of useFetch, and not lazy loading heavy components below the fold.

Should I use useFetch or useAsyncData?

Use useFetch for most cases - it's simpler and handles caching automatically. Use useAsyncData when you need more control over the cache key or want to combine multiple data sources.

How do I lazy load components in Nuxt?

Prefix any component with Lazy (e.g., <LazyHeavyChart />) and Nuxt automatically code-splits and lazy loads it. For more control, use defineAsyncComponent.

What's the best way to handle fonts in Nuxt?

Use @nuxtjs/fontaine for automatic font fallback matching (eliminates CLS), or @nuxtjs/google-fonts with preload enabled. Avoid loading fonts via CSS @import.

Does ClientOnly hurt performance?

<ClientOnly> content ships JavaScript and doesn't render on the server, so overusing it hurts both load time and SEO. Only wrap components that genuinely need browser APIs or heavy client-side interactivity.

What's next

Want to see exactly where your Nuxt app is losing performance points? Run it through PageSpeedFix - we'll identify the specific issues and show you the Nuxt-specific code to fix them.

Related guides: