nuxt

SKILL.md

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 asset
  • swr: N — server/proxy cache for N seconds, stale-while-revalidate
  • isr: N — CDN cache until next deploy (Vercel/Netlify); isr: true means persist indefinitely
  • ssr: false — browser-only rendering (SPA mode for that route)
  • redirect: '/new' — server-side redirect
  • headers: {} — 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 / shallowReactive for large objects not needing deep reactivity
  • v-once for static subtrees that never change
  • v-memo to skip re-renders when dependencies are unchanged
  • computed to 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

  1. Bundle analysisnpx nuxi analyze generates a visual treemap; identify large dependencies.
  2. Nuxt DevTools — Timeline, Render Tree, and Inspect tabs reveal component render costs and file sizes.
  3. Chrome DevTools — Performance panel shows LCP/CLS live; Lighthouse gives actionable scores.
  4. PageSpeed Insights — Combine lab + real-world field data for production auditing.
  5. WebPageTest — Test from multiple global regions and network conditions.

Additional Resources

Reference Files

  • references/performance.md — Detailed breakdown of all performance techniques, routeRules options, and Core Web Vitals targets
  • references/rendering.md — In-depth rendering mode trade-offs, hydration mismatch prevention, and deployment targets
Weekly Installs
2
GitHub Stars
1
First Seen
Today
Installed on
opencode2
amp1
cline1
cursor1
kimi-cli1
codex1