nuxt
Nuxt Best Practices
Guidance for building high-performance Nuxt 4 applications following official best practices. Covers rendering strategies, performance optimization, data fetching, component patterns, and profiling.
Rendering Strategies
Choose the Right Rendering Mode per Route
Nuxt 4 supports hybrid rendering via routeRules. Assign rendering strategies per route rather than using a single global mode:
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true }, // Static at build time
'/products/**': { swr: 3600 }, // Cached, revalidated in background
'/blog': { isr: 3600 }, // CDN-cached, revalidated hourly
'/admin/**': { ssr: false }, // Client-side only
'/api/**': { cors: true },
},
})
Route rule reference:
prerender: true— generate at build time, serve as static assetswr: N— server/proxy cache for N seconds, stale-while-revalidateisr: N— CDN cache until next deploy (Vercel/Netlify);isr: truemeans persist indefinitelyssr: false— browser-only rendering (SPA mode for that route)redirect: '/new'— server-side redirectheaders: {}— add custom response headers (e.g., long cache on assets)
Default to Universal Rendering
Universal rendering (SSR + hydration) is the default and best choice for content-oriented apps (blogs, e-commerce, marketing sites). It delivers:
- Immediate HTML visible to users and crawlers
- Full interactivity after hydration
- Better Core Web Vitals scores (LCP, CLS)
Edge-Side Rendering
Deploy to CDN edge servers (Cloudflare Workers, Vercel Edge, Netlify Edge) for reduced latency. Nuxt's Nitro engine supports these out of the box — no code changes required, only a different build preset.
See references/rendering.md for a full breakdown of trade-offs and deployment targets.
Component Best Practices
Lazy-Load Non-Critical Components
Prefix any component with Lazy to defer its JavaScript until needed. This directly reduces initial bundle size and improves Time to Interactive (TTI):
<script setup lang="ts">
const show = ref(false)
</script>
<template>
<div>
<button @click="show = true">Load list</button>
<LazyMountainsList v-if="show" />
</div>
</template>
Use Lazy Hydration (Nuxt 3.16+)
Defer hydration of components until they enter the viewport or the browser is idle. This improves TTI without sacrificing SSR-rendered content:
<template>
<!-- Hydrate only when scrolled into view -->
<LazyCommentSection hydrate-on-visible />
<!-- Hydrate when browser is idle -->
<LazyAnalyticsWidget hydrate-on-idle />
<!-- Hydrate on user interaction -->
<LazyChatWidget hydrate-on-interaction />
</template>
Avoid Overusing Plugins
Plugins execute during the hydration phase and block interactivity. Audit plugins regularly — if logic does not need to run globally at startup, move it to a composable or utility function instead.
Data Fetching
Use useFetch and useAsyncData
These composables deduplicate server-side fetches. Data fetched on the server is serialized into the page payload and reused by the client — no double fetch:
// Good: data transferred via payload, no duplicate network request
const { data } = await useFetch('/api/products')
// Bad: runs separately on server and client
const data = await $fetch('/api/products')
Keep Composables Synchronous at the Top Level
Vue and Nuxt composables rely on a synchronous lifecycle context. Do not call composables after an await outside of <script setup>, defineNuxtComponent, defineNuxtPlugin, or defineNuxtRouteMiddleware:
// Bad
const data = await someAsyncOperation()
const config = useRuntimeConfig() // context lost
// Good — call composable before await, or inside <script setup>
const config = useRuntimeConfig()
const data = await someAsyncOperation()
Performance Optimization
Images: Use <NuxtImg>
Replace all <img> tags with <NuxtImg> (requires @nuxt/image). It auto-converts to WebP/Avif, resizes, and generates responsive sizes:
<!-- Above-the-fold / LCP image -->
<NuxtImg
src="/hero.jpg"
format="webp"
preload
loading="eager"
fetch-priority="high"
width="1200"
height="600"
/>
<!-- Below-the-fold image -->
<NuxtImg
src="/feature.jpg"
format="webp"
loading="lazy"
fetch-priority="low"
width="600"
height="400"
/>
Always set width and height to prevent layout shift (CLS).
Fonts: Use Nuxt Fonts
Add @nuxt/fonts to automatically self-host fonts, inject @font-face rules, and generate fallback metrics that minimize CLS. No manual <link rel="preload"> required.
Third-Party Scripts: Use Nuxt Scripts
Add @nuxt/scripts to load analytics, embeds, and social widgets without blocking the main thread. Scripts support deferred triggers and typed proxies:
const { proxy } = useScriptGoogleAnalytics({
id: 'G-XXXXXXXX',
scriptOptions: { trigger: 'manual' },
})
// Safe to call before script loads — events are queued
proxy.gtag('event', 'page_view')
Leverage Smart Prefetching via <NuxtLink>
<NuxtLink> automatically prefetches JavaScript for in-viewport links. To reduce bandwidth on large sites, switch to interaction-based prefetching:
// nuxt.config.ts
export default defineNuxtConfig({
experimental: {
defaults: {
nuxtLink: { prefetchOn: 'interaction' },
},
},
})
Apply Vue-Level Optimizations
Nuxt apps are Vue apps — apply Vue performance primitives:
shallowRef/shallowReactivefor large objects not needing deep reactivityv-oncefor static subtrees that never changev-memoto skip re-renders when dependencies are unchangedcomputedto cache derived state rather than recalculating in templates
Auto-Imports
Nuxt auto-imports components from app/components/, composables from app/composables/, and utilities from app/utils/. Only used exports appear in the production bundle — no manual tree-shaking needed.
Place server-only utilities in server/utils/ — they are also auto-imported within the server/ directory.
To make imports explicit (useful for clarity or monorepos), use the #imports alias:
import { ref, computed, useFetch } from '#imports'
Common Pitfalls
| Problem | Solution |
|---|---|
| Double data fetch (server + client) | Use useFetch / useAsyncData instead of raw $fetch |
| Plugins blocking hydration | Move non-global logic to composables |
| Large bundles | Run nuxi analyze; lazy-load large components |
| Layout shift from images | Always set width/height on <NuxtImg> |
| Unoptimized fonts | Add @nuxt/fonts |
| INP degradation from third-party scripts | Add @nuxt/scripts with deferred triggers |
| Deep reactivity on large objects | Use shallowRef / shallowReactive |
Composable called after await |
Call composables synchronously before async operations |
| Unused dependencies bloating bundle | Audit package.json; remove unused packages |
Profiling Workflow
- Bundle analysis —
npx nuxi analyzegenerates a visual treemap; identify large dependencies. - Nuxt DevTools — Timeline, Render Tree, and Inspect tabs reveal component render costs and file sizes.
- Chrome DevTools — Performance panel shows LCP/CLS live; Lighthouse gives actionable scores.
- PageSpeed Insights — Combine lab + real-world field data for production auditing.
- WebPageTest — Test from multiple global regions and network conditions.
Additional Resources
Reference Files
references/performance.md— Detailed breakdown of all performance techniques,routeRulesoptions, and Core Web Vitals targetsreferences/rendering.md— In-depth rendering mode trade-offs, hydration mismatch prevention, and deployment targets