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>withpreloadfor hero images - Preload critical fonts
- Minimize blocking JavaScript
CLS
- Always set
widthandheighton images - Use Fontaine for automatic font fallback matching
- Reserve space for dynamic content
INP
- Minimize
<ClientOnly>usage - Use
Lazyprefix 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:
- Next.js vs Nuxt Performance - Comparing the two frameworks with real benchmarks
- Vite + React Performance - Vite optimization patterns that apply to Nuxt