A comprehensive checklist of performance optimizations every Nuxt developer should master.
1. Component Optimization
Use defineAsyncComponent for Code Splitting
Lazy load components that aren't immediately needed:
<script setup>
// Heavy components only load when needed
const HeavyChart = defineAsyncComponent(() => import('~/components/HeavyChart.vue'));
const VideoPlayer = defineAsyncComponent(() => import('~/components/VideoPlayer.vue'));
</script>
<template>
<div>
<Suspense>
<HeavyChart v-if="showChart" />
<template #fallback>
<div>Loading chart...</div>
</template>
</Suspense>
</div>
</template>
Impact: Reduces initial bundle size, faster Time to Interactive.
Client-Only Components
Don't render heavy components on the server:
<template>
<div>
<ClientOnly>
<InteractiveMap />
<template #fallback>
<div>Loading map...</div>
</template>
</ClientOnly>
</div>
</template>
Use for: Third-party widgets, maps, charts, browser-only APIs.
2. Data Fetching Optimization
Use useFetch with Key
Prevent duplicate requests with proper keys:
const { data, error } = await useFetch('/api/posts', {
key: 'posts-list',
lazy: true,
server: true,
transform: (data) => data.posts // Only return what you need
});
Lazy Data Fetching
Defer non-critical data:
const { data: criticalData } = await useFetch('/api/critical');
// Lazy load non-critical data
const { data: analytics } = useFetch('/api/analytics', {
lazy: true,
server: false // Client-side only
});
Parallel Data Fetching
Fetch multiple resources simultaneously:
const [posts, users, settings] = await Promise.all([
useFetch('/api/posts'),
useFetch('/api/users'),
useFetch('/api/settings')
]);
3. Route Rules & Caching
Static Site Generation (SSG)
Pre-render pages at build time:
// nuxt.config.js
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true },
'/about': { prerender: true },
'/blog/**': { isr: 3600 } // ISR: Revalidate every hour
}
});
Stale-While-Revalidate (SWR)
Serve cached content while updating in background:
routeRules: {
'/api/**': { swr: true },
'/blog': { swr: 3600 }, // Cache for 1 hour
'/products/**': { swr: 1800 } // Cache for 30 min
}
Static Asset Caching
Long-term cache for unchanging assets:
routeRules: {
'/_nuxt/**': {
headers: {
'cache-control': 'public, max-age=31536000, immutable'
}
}
}
4. Image Optimization
Use NuxtImg with Lazy Loading
<template>
<NuxtImg
src="/images/hero.jpg"
width="800"
height="600"
loading="lazy"
format="webp"
quality="80"
alt="Hero image"
/>
</template>
Configure Image Module
// nuxt.config.js
export default defineNuxtConfig({
image: {
formats: ['webp', 'avif'],
quality: 80,
screens: {
xs: 320,
sm: 640,
md: 768,
lg: 1024,
xl: 1280
},
densities: [1, 2]
}
});
Use NuxtPicture for Art Direction
<template>
<NuxtPicture
src="/images/hero.jpg"
:img-attrs="{ alt: 'Hero' }"
:modifiers="{
fit: 'cover',
format: 'webp'
}"
/>
</template>
5. Bundle Optimization
Analyze Your Bundle
# Build with analysis
npx nuxi analyze
# Or use build command
npm run build
Auto-Import Configuration
Optimize auto-imports to reduce bundle size:
// nuxt.config.js
export default defineNuxtConfig({
imports: {
presets: [
{
from: 'lodash-es',
imports: ['debounce', 'throttle']
}
]
}
});
Tree Shaking for Third-Party Libraries
// Import only what you need
import debounce from 'lodash-es/debounce';
// Not the entire library
// ❌ import _ from 'lodash';
6. Composables for Reusability
Create Performance-Optimized Composables
// composables/useDebounce.js
export const useDebounce = (fn, delay = 300) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
};
// Usage
const handleSearch = useDebounce((query) => {
fetchResults(query);
}, 300);
Lazy Composables
// Only load when needed
const { data } = await useLazyAsyncData('heavy-data', () =>
$fetch('/api/heavy-data')
);
7. Payload Optimization
Reduce Payload Size
// nuxt.config.js
export default defineNuxtConfig({
experimental: {
payloadExtraction: true,
renderJsonPayloads: true,
treeshakeClientOnly: true
}
});
Data Transformation in Queries
Transform data on the server to reduce payload:
const { data } = await useFetch('/api/posts', {
transform: (posts) => {
// Only return what the UI needs
return posts.map(({ id, title, excerpt }) => ({
id,
title,
excerpt
}));
}
});
8. Resource Hints & Preloading
DNS Prefetch & Preconnect
// nuxt.config.js
app: {
head: {
link: [
// DNS prefetch for third parties
{ rel: 'dns-prefetch', href: 'https://cdn.example.com' },
// Preconnect for critical resources
{
rel: 'preconnect',
href: 'https://fonts.googleapis.com',
crossorigin: 'anonymous'
}
]
}
}
Preload Critical Assets
app: {
head: {
link: [
{
rel: 'preload',
as: 'font',
type: 'font/woff2',
href: '/fonts/main.woff2',
crossorigin: 'anonymous'
}
]
}
}
Prefetch Routes
<template>
<!-- Prefetch on hover -->
<NuxtLink to="/about" prefetch>About</NuxtLink>
<!-- No prefetch for external links -->
<NuxtLink to="/heavy-page" :prefetch="false">Heavy Page</NuxtLink>
</template>
9. State Management Performance
Use Pinia with Smart Caching
// stores/posts.js
export const usePostsStore = defineStore('posts', {
state: () => ({
posts: [],
lastFetch: null
}),
actions: {
async fetchPosts() {
// Cache for 5 minutes
if (this.lastFetch && Date.now() - this.lastFetch < 300000) {
return this.posts;
}
const data = await $fetch('/api/posts');
this.posts = data;
this.lastFetch = Date.now();
return data;
}
}
});
Avoid Reactivity for Large Datasets
// Use shallowRef for large arrays/objects
const largeDataset = shallowRef([]);
// Or mark as raw if no reactivity needed
const staticData = markRaw(hugeDataset);
10. Server-Side Optimization
Use Server Routes for Heavy Logic
// server/api/process.post.js
export default defineEventHandler(async (event) => {
const body = await readBody(event);
// Heavy processing on server
const result = await heavyProcessing(body);
return result;
});
Nitro Presets for Performance
// nuxt.config.js
export default defineNuxtConfig({
nitro: {
preset: 'vercel', // or 'netlify', 'cloudflare'
compressPublicAssets: true,
minify: true
}
});
11. Hybrid Rendering Strategies
Per-Route Rendering
routeRules: {
'/': { prerender: true }, // SSG
'/blog/**': { swr: 3600 }, // ISR
'/api/**': { cors: true }, // API routes
'/admin/**': { ssr: false } // CSR only
}
Choose wisely:
- SSG (prerender): Static content, rarely changes
- SSR: Dynamic, personalized content
- ISR (swr): Semi-static, periodic updates
- CSR (ssr: false): Client-only, requires auth
12. Critical CSS & Fonts
Inline Critical CSS
// nuxt.config.js
export default defineNuxtConfig({
experimental: {
inlineSSRStyles: true
}
});
Font Optimization
// Use font-display: swap
app: {
head: {
link: [
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap'
}
]
}
}
13. Third-Party Script Management
Use useHead with Script
useHead({
script: [
{
src: 'https://analytics.example.com/script.js',
defer: true,
// Or use async: true
}
]
});
Lazy Load Analytics
// plugins/analytics.client.js
export default defineNuxtPlugin(() => {
// Only load on client, after page load
if (typeof window !== 'undefined') {
window.addEventListener('load', () => {
// Initialize analytics
});
}
});
14. Build-Time Optimization
Reduce Build Time
// nuxt.config.js
export default defineNuxtConfig({
vite: {
build: {
rollupOptions: {
output: {
manualChunks(id) {
// Split vendor chunks
if (id.includes('node_modules')) {
return 'vendor';
}
}
}
}
}
}
});
Enable TypeScript Performance Mode
// nuxt.config.js
export default defineNuxtConfig({
typescript: {
typeCheck: 'build', // Only on build, not during dev
strict: true
}
});
15. Database & API Optimization
Use Caching Headers
// server/api/posts.get.js
export default defineEventHandler(async (event) => {
const posts = await fetchPosts();
// Set cache headers
setResponseHeaders(event, {
'cache-control': 'public, max-age=3600, s-maxage=7200'
});
return posts;
});
Implement API Response Caching
// server/utils/cache.js
const cache = new Map();
export const getCached = async (key, fetchFn, ttl = 3600000) => {
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data;
}
const data = await fetchFn();
cache.set(key, { data, timestamp: Date.now() });
return data;
};
16. Monitoring & Debugging
Use Nuxt DevTools
// nuxt.config.js
export default defineNuxtConfig({
devtools: {
enabled: true,
timeline: {
enabled: true
}
}
});
Performance Profiling
// Use console.time for profiling
const start = performance.now();
await heavyOperation();
console.log(`Operation took ${performance.now() - start}ms`);
Monitor Core Web Vitals
// plugins/web-vitals.client.js
export default defineNuxtPlugin(() => {
if (process.client) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(console.log);
getFID(console.log);
getFCP(console.log);
getLCP(console.log);
getTTFB(console.log);
});
}
});
17. Nuxt 3 Specific Features
Auto-Import Optimization
Nuxt auto-imports are tree-shakable:
// Automatically available, tree-shaken
const route = useRoute();
const router = useRouter();
const { data } = await useFetch('/api/data');
Tip: Verify imports are being tree-shaken in build output.
Nitro Server Engine
Leverage Nitro's built-in features:
// nuxt.config.js
export default defineNuxtConfig({
nitro: {
compressPublicAssets: true, // Gzip/Brotli
minify: true,
prerender: {
crawlLinks: true,
routes: ['/sitemap.xml']
}
}
});
UseAsyncData for Complex Fetching
const { data, pending, refresh } = await useAsyncData(
'complex-data',
async () => {
const [posts, users] = await Promise.all([
$fetch('/api/posts'),
$fetch('/api/users')
]);
return { posts, users };
},
{
lazy: true,
server: true,
watch: [someRef] // Re-fetch when ref changes
}
);
18. CSS & Style Optimization
Scoped Styles with Deep Selectors
<style scoped>
.parent {
color: blue;
}
/* Use :deep() for child components */
.parent :deep(.child) {
color: red;
}
</style>
Extract Critical CSS
// nuxt.config.js
export default defineNuxtConfig({
experimental: {
inlineSSRStyles: true
}
});
Use CSS Variables for Theming
// Faster than JavaScript theme switching
:root {
--color-primary: #ff6b35;
--color-text: #333;
}
.button {
background: var(--color-primary);
color: var(--color-text);
}
19. Middleware & Guards
Optimize Middleware Execution
// middleware/auth.js
export default defineNuxtRouteMiddleware((to, from) => {
// Skip if already authenticated
if (process.server && to.meta.public) {
return;
}
const user = useAuthUser();
if (!user.value) {
return navigateTo('/login');
}
});
Use Global vs Route Middleware Wisely
// Global middleware runs on every route (use sparingly)
// middleware/analytics.global.js
// Route-specific middleware (better performance)
definePageMeta({
middleware: ['auth', 'analytics']
});
20. Error Handling Performance
Custom Error Pages
<!-- error.vue -->
<script setup>
const props = defineProps({
error: Object
});
// Don't fetch unnecessary data on error pages
useMeta({
title: 'Error',
description: 'An error occurred'
});
</script>
Graceful Degradation
const { data, error } = await useFetch('/api/data', {
onRequestError() {
// Handle network errors
return { fallback: true };
},
onResponseError() {
// Handle API errors
return { fallback: true };
}
});
Performance Checklist
Before deploying:
- Run
npm run analyze
to check bundle size - All images use
loading="lazy"
and modern formats - Critical routes are prerendered or use SWR
- Heavy components are code-split with
defineAsyncComponent
- API responses have proper cache headers
- Third-party scripts load asynchronously
- Fonts use
font-display: swap
- No console.log in production code
- Lighthouse score > 90 for all pages
- Test on slow 3G network
- Check payload sizes in Network tab
- Verify Core Web Vitals (LCP < 2.5s, FID < 100ms, CLS < 0.1)
Common Nuxt Performance Mistakes
1. Over-fetching Data
// ❌ Bad - Fetching entire objects
const { data } = await useFetch('/api/posts');
// ✅ Good - Transform to get only what's needed
const { data } = await useFetch('/api/posts', {
transform: (posts) => posts.map(({ id, title, excerpt }) => ({
id, title, excerpt
}))
});
2. Not Using Proper Keys
// ❌ Bad - No key, might duplicate requests
const { data } = await useFetch('/api/posts');
// ✅ Good - With unique key
const { data } = await useFetch('/api/posts', {
key: 'posts-list'
});
3. Blocking the Critical Path
// ❌ Bad - Waits for both before rendering
const posts = await useFetch('/api/posts');
const comments = await useFetch('/api/comments');
// ✅ Good - Render while loading comments
const posts = await useFetch('/api/posts');
const { data: comments } = useFetch('/api/comments', { lazy: true });
4. Not Utilizing Server Routes
// ❌ Bad - Heavy logic in component
const processData = (rawData) => {
// Expensive operations
return processed;
};
// ✅ Good - Process on server
// server/api/process.post.js
export default defineEventHandler(async (event) => {
const data = await readBody(event);
return processData(data);
});
5. Ignoring Hydration Mismatch
// ❌ Bad - Different content server vs client
<div>{{ new Date().toLocaleString() }}</div>
// ✅ Good - Consistent or client-only
<ClientOnly>
<div>{{ new Date().toLocaleString() }}</div>
<template #fallback>
<div>Loading...</div>
</template>
</ClientOnly>
Advanced Nuxt Performance Patterns
1. Intersection Observer for Lazy Loading
const { data } = await useLazyAsyncData('lazy-content', async () => {
return $fetch('/api/content');
}, {
server: false // Client-side only
});
// Trigger when element is visible
const target = ref(null);
const { stop } = useIntersectionObserver(target, ([{ isIntersecting }]) => {
if (isIntersecting) {
// Fetch data
refresh();
stop();
}
});
2. Incremental Static Regeneration (ISR)
routeRules: {
'/blog/**': {
isr: 3600, // Revalidate every hour
// Or
swr: 3600 // Stale-while-revalidate
}
}
3. Edge Rendering
Deploy to edge for lower latency:
// nuxt.config.js
export default defineNuxtConfig({
nitro: {
preset: 'vercel-edge',
// Or 'cloudflare-pages', 'netlify-edge'
}
});
4. Streaming SSR
For large pages, stream HTML chunks:
// Automatically enabled in Nuxt 3
// Just ensure async components use Suspense
Testing Performance
Local Testing
# Build for production
npm run build
# Preview production build
npm run preview
# Test with throttling in Chrome DevTools
# Network tab → Slow 3G
Lighthouse CI
Add to your CI/CD pipeline:
# Install
npm install -g @lhci/cli
# Run
lhci autorun --collect.url=http://localhost:3000
Real User Monitoring
// Track actual user performance
export default defineNuxtPlugin(() => {
if (process.client) {
window.addEventListener('load', () => {
const perfData = performance.getEntriesByType('navigation')[0];
console.log('Load time:', perfData.loadEventEnd - perfData.fetchStart);
});
}
});
Performance Budgets
Set and enforce performance budgets:
// lighthouse-budget.json
{
"resourceSizes": [
{
"resourceType": "script",
"budget": 300
},
{
"resourceType": "image",
"budget": 500
}
]
}
Nuxt 3 vs Nuxt 2 Performance Wins
Nuxt 3 improvements out of the box:
- ⚡ Nitro engine (faster builds, smaller bundles)
- 🎯 Native ES modules (better tree-shaking)
- 📦 Auto code splitting
- 🔥 Faster HMR in development
- 💾 Smaller bundle sizes (~50% reduction)
- 🚀 Edge-ready by default
Key Takeaways
- Measure first - Use Chrome DevTools, Lighthouse, and Nuxt DevTools
- Use route rules - SWR and ISR are your friends
- Code split aggressively - Every heavy component should be async
- Optimize images - Use NuxtImg with modern formats
- Cache strategically - Server, CDN, and browser caching
- Server-side processing - Move heavy logic to server routes
- Monitor continuously - Track real user metrics
Resources
Remember
Test on real devices, real networks, and real scenarios. Your users will thank you.
Last updated: October 2025