
Web application performance isn't just a technical metric. It directly impacts conversion rates, SEO, and user satisfaction. Nuxt provides a powerful arsenal of optimization tools, but they need to be applied correctly.
In this article, we'll break down all the key aspects of Nuxt application speed—from built-in mechanisms to advanced techniques.
Google evaluates site performance through Core Web Vitals—three key metrics:
| Metric | What it Measures | Target Value |
|---|---|---|
| LCP (Largest Contentful Paint) | Time to load the largest element | < 2.5s |
| CLS (Cumulative Layout Shift) | Visual stability | < 0.1 |
| INP (Interaction to Next Paint) | Responsiveness to user interaction | < 200ms |
These metrics directly influence Google rankings. Nuxt is optimized to achieve high scores out of the box, but real-world results depend on proper configuration.
Important: INP replaced FID (First Input Delay) in March 2024 as an official Core Web Vitals metric.
Nuxt automatically applies a series of optimizations without any extra configuration.
Each page and asynchronous component is automatically split into separate chunks. The user only downloads the necessary code:
// Nuxt automatically splits into chunks:
// - pages/index.vue → _nuxt/index-[hash].js
// - pages/about.vue → _nuxt/about-[hash].js
// - components/Heavy.vue → _nuxt/Heavy-[hash].js (if lazy)
Nuxt uses Vite as its build tool. Vite provides:
The <NuxtLink> component automatically prefetches pages when the link enters the viewport:
<template>
<!-- Prefetch will trigger automatically on entering the viewport -->
<NuxtLink to="/products">Catalog</NuxtLink>
<!-- Disable prefetch for a specific link -->
<NuxtLink to="/heavy-page" :prefetch="false">
Heavy Page
</NuxtLink>
</template>
Nuxt supports hybrid rendering—different strategies for different routes. This is one of the most powerful optimization tools.
| Mode | Description | When to Use |
|---|---|---|
prerender | Generate HTML at build time | Static content (homepage, about us) |
swr | Stale-While-Revalidate | Frequently updated content |
isr | Incremental Static Regeneration | Content with CDN caching |
ssr: false | Client-side only | Admin panels, dashboards |
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Static homepage
'/': { prerender: true },
// Catalog: 1-hour cache, background refresh
'/products/**': { swr: 3600 },
// Blog: CDN cache with ISR
'/blog/**': { isr: true },
// Admin: client-side only
'/admin/**': { ssr: false },
// API: CORS + cache
'/api/**': {
cors: true,
cache: { maxAge: 60 }
}
}
})
Tip: Use
defineRouteRulesdirectly within a page component for local configuration.
These two techniques are often confused, but they solve different problems.
This defers the loading of a component's code until it's needed:
<script setup>
const showModal = ref(false)
</script>
<template>
<button @click="showModal = true">Open</button>
<!-- The code will only be loaded when showModal = true -->
<LazyModal v-if="showModal" @close="showModal = false" />
</template>
Just add the Lazy prefix to the component name—Nuxt does the rest.
This defers the hydration (activation of interactivity) of an already-loaded component. The HTML is rendered on the server, but the JavaScript is activated later.
Available strategies:
<template>
<!-- Hydrate when it enters the viewport -->
<LazyFooter hydrate-on-visible />
<!-- Hydrate when the browser is idle -->
<LazySidebar hydrate-on-idle />
<!-- Hydrate on interaction -->
<LazyComments hydrate-on-interaction="click" />
<!-- Hydrate on a media query -->
<LazyMobileMenu hydrate-on-media-query="(max-width: 768px)" />
<!-- Never hydrate (purely static content) -->
<LazyStaticBanner hydrate-never />
</template>
Important: Lazy hydration significantly improves Time to Interactive (TTI), especially for components below the viewport.
For content that requires no interactivity at all, use Server Components:
<!-- components/RenderMarkdown.server.vue -->
<script setup>
const props = defineProps<{ source: string }>()
</script>
<template>
<div v-html="renderMarkdown(props.source)" />
</template>
The .server.vue extension means:
An alternative is <NuxtIsland> for wrapping any component:
<template>
<NuxtIsland name="HeavyChart" :props="{ data }" />
</template>
Unoptimized images are the main cause of poor LCP. Nuxt Image provides a comprehensive solution.
npx nuxi module add @nuxt/image
<template>
<!-- Instead of <img> -->
<NuxtImg
src="/hero.jpg"
width="1200"
height="600"
alt="Hero image"
format="webp"
loading="lazy"
/>
<!-- For an LCP image -->
<NuxtImg
src="/hero.jpg"
width="1200"
height="600"
alt="Hero image"
:loading="undefined"
fetchpriority="high"
/>
</template>
<template>
<!-- ❌ Bad: lazy for LCP -->
<NuxtImg src="/hero.jpg" loading="lazy" />
<!-- ✅ Good: eager + fetchpriority for LCP -->
<NuxtImg
src="/hero.jpg"
loading="eager"
fetchpriority="high"
/>
<!-- ✅ Good: responsive with sizes -->
<NuxtImg
src="/product.jpg"
sizes="(max-width: 640px) 100vw, 50vw"
:modifiers="{ format: 'webp', quality: 80 }"
/>
</template>
| Property | For LCP | For Others |
|---|---|---|
loading | eager or not specified | lazy |
fetchpriority | high | low or not specified |
format | webp or avif | webp or avif |
Fonts affect CLS and LCP. Nuxt Fonts automates optimization.
npx nuxi module add @nuxt/fonts
The module automatically:
font-family in your CSS/* Nuxt Fonts will process this automatically */
body {
font-family: 'Inter', sans-serif;
}
Nuxt Fonts uses fontaine to calculate metrics for a system fallback font that closely matches the web font:
/* Generated automatically */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
size-adjust: 107%;
}
This minimizes the text "jump" when the web font loads.
The size of the JavaScript bundle is critical for LCP and TTI.
# Visualize chunk sizes
npx nuxi analyze
This command generates an interactive treemap showing the size of all modules.
Problem: Importing an entire library
// ❌ Bad: the whole lodash (~70KB)
import _ from 'lodash'
// ✅ Good: only the needed function (~4KB)
import debounce from 'lodash/debounce'
Problem: Heavy libraries without lazy loading
<script setup>
// ❌ Bad: loaded immediately
import { Chart } from 'chart.js'
// ✅ Good: dynamic import
const { Chart } = await import('chart.js')
</script>
Problem: Duplicate dependencies
# Check for duplicates
npm ls moment
npm ls date-fns
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
build: {
rollupOptions: {
output: {
manualChunks: {
// Move large libraries to separate chunks
'vendor-chart': ['chart.js'],
'vendor-editor': ['@tiptap/core', '@tiptap/vue-3']
}
}
}
}
}
})
Built-in tools for local development:
// nuxt.config.ts
export default defineNuxtConfig({
devtools: { enabled: true }
})
| Tab | What it Shows |
|---|---|
| Performance | LCP, CLS, INP in real-time |
| Lighthouse | Comprehensive audit with recommendations |
| Network | Resource loading waterfall |
For production testing, use PageSpeed Insights. It shows:
<!-- ❌ Critical mistake -->
<NuxtImg src="/hero.jpg" loading="lazy" />
<!-- ✅ Correct -->
<NuxtImg src="/hero.jpg" fetchpriority="high" />
The LCP element must be loaded with the highest priority.
// ❌ Bad: everything is reactive
const data = ref({
config: { ... }, // Never changes
items: [] // Changes
})
// ✅ Good: only what's needed is reactive
const CONFIG = { ... } // A regular constant
const items = ref([]) // A reactive array
// ❌ Bad: event listener without cleanup
onMounted(() => {
window.addEventListener('resize', handleResize)
})
// ✅ Good: with cleanup
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
Every plugin is executed during hydration. Consider alternatives:
// ❌ Bad: blocks rendering
useHead({
script: [{ src: 'https://analytics.com/script.js' }]
})
// ✅ Good: use Nuxt Scripts
// npx nuxi module add @nuxt/scripts
Nuxt Scripts loads third-party scripts with minimal impact on performance.
Before deploying, check the following:
nuxi analyze—bundle < 250KB per pageloading="lazy"routeRules for static pages