Nuxt Performance: From Good Lighthouse Scores to Great Ones
Advanced Nuxt performance techniques — code splitting, lazy hydration, bundle analysis, prefetching, edge caching, and the optimizations that move Lighthouse from 80 to 98.

James Ross Jr.
Strategic Systems Architect & Enterprise Software Developer
A Lighthouse score of 80 is table stakes. Most Nuxt applications hit that without much effort because the framework's defaults are sensible. Getting to 95+ requires deliberate choices about what JavaScript ships to the browser, when it executes, and how aggressively you cache at every layer.
I have tuned the performance of enough production Nuxt applications that I have a consistent set of techniques that move the needle. These are not theoretical — they are patterns I apply on client projects and measure the impact of.
Understand Your Baseline First
Before optimizing anything, understand what you are optimizing. Run a Lighthouse audit in Chrome DevTools with throttling enabled (simulates a mobile 4G connection). Look at the three numbers that actually matter:
LCP (Largest Contentful Paint): When does the main content appear? Target: under 2.5 seconds.
INP (Interaction to Next Paint): How quickly does the page respond to input? Target: under 200ms.
CLS (Cumulative Layout Shift): How much does the layout shift unexpectedly? Target: under 0.1.
Then open the Network tab and the Coverage tab. The Network tab shows you exactly what is being downloaded and how large each file is. The Coverage tab shows you how much of that downloaded JavaScript is actually executed.
The Coverage tab is often a revelation. On unoptimized applications, I routinely see 40-60% of downloaded JavaScript going unused on any given page. That is waste you can eliminate.
Bundle Analysis
Install the bundle analyzer:
npm install --save-dev rollup-plugin-visualizer
// nuxt.config.ts
vite: {
plugins: [
process.env.ANALYZE && visualizer({
open: true,
gzipSize: true,
brotliSize: true,
}),
].filter(Boolean),
},
Run ANALYZE=true npm run build to generate an interactive treemap of your bundle. Look for:
- Large libraries that could be replaced with smaller alternatives
- Libraries that are imported but barely used
- Duplicate dependencies being bundled multiple times
Common findings: lodash imported in full instead of individual functions, large chart libraries included for a single chart on one page, moment.js instead of date-fns.
Lazy Loading Routes
Nuxt lazy-loads routes by default — each page becomes its own JavaScript chunk. But components imported directly are included in the current chunk. Prefix large components with Lazy to defer them:
<!-- Direct import: included in the current page bundle -->
<HeavyChart :data="chartData" />
<!-- Lazy import: fetched only when the component renders -->
<LazyHeavyChart :data="chartData" />
For components that might not render at all (error states, empty states, modals), lazy loading is especially valuable:
<LazyErrorBoundary v-if="hasError" :error="error" />
<LazyEmptyState v-else-if="items.length === 0" />
<LazyConfirmModal v-if="showConfirm" @confirm="handleConfirm" />
Granular Code Splitting
For large features that are only used by some users, split them into separate chunks:
// nuxt.config.ts
vite: {
build: {
rollupOptions: {
output: {
manualChunks: {
'charts': ['chart.js', 'vue-chartjs'],
'editor': ['@tiptap/core', '@tiptap/vue-3'],
'maps': ['leaflet', '@vue-leaflet/vue-leaflet'],
},
},
},
},
},
These chunks only download when a component that uses them renders for the first time. A user who never opens the map view never downloads the maps bundle.
Defer Non-Critical JavaScript
Third-party scripts — analytics, chat widgets, marketing pixels — should never block page rendering:
<!-- plugins/analytics.client.ts -->
<script setup lang="ts">
onMounted(() => {
// Delay until after the page is interactive
requestIdleCallback(() => {
// Load analytics
window.dataLayer = window.dataLayer || []
// ... Google Analytics initialization
})
})
</script>
The <ScriptGoogleAnalytics> and similar components from @nuxt/scripts handle this pattern with a clean API and a Partytown integration for true off-main-thread execution.
// nuxt.config.ts
scripts: {
registry: {
googleAnalytics: {
id: 'G-XXXXXXXXXX',
scriptOptions: {
trigger: 'idle', // Load after page is idle
},
},
},
},
Server Component Islands
For pages with mostly static content and a few interactive islands, use Nuxt's server components to reduce hydration cost:
<!-- components/StaticArticle.server.vue -->
<!-- This renders on the server and ships NO JavaScript to the client -->
<template>
<article class="prose">
<h1>{{ title }}</h1>
<div v-html="content" />
<AuthorCard :author="author" />
</article>
</template>
Components in .server.vue files are rendered on the server and sent as HTML. They do not ship JavaScript to the client, do not hydrate, and cannot have client-side interactivity. For static content sections of a page, this is a significant bundle size reduction.
Interactive elements on the same page use regular components and hydrate normally.
Payload Hydration
When Nuxt SSR fetches data on the server, it includes the data in the HTML as a serialized payload. This allows the client to read the data directly without re-fetching it during hydration. This works automatically with useAsyncData and useFetch.
Make sure you are using consistent keys so deduplication works:
// This key ensures the same data is not fetched twice
const { data } = await useAsyncData('homepage-posts', () =>
$fetch('/api/posts?featured=true')
)
Without a stable key, the same API call might happen on the server, be included in the payload, and then fire again on the client — doubling your API load and slowing hydration.
Prefetching for Perceived Performance
Make navigation feel instant by prefetching pages before the user clicks:
// nuxt.config.ts
experimental: {
payloadExtraction: true,
},
Nuxt prefetches page payloads when links enter the viewport by default. For pages you know users will navigate to, you can prefetch manually:
<script setup lang="ts">
const router = useRouter()
// Prefetch when the cursor enters the button
function prefetch() {
router.prefetch('/dashboard')
}
</script>
<template>
<NuxtLink to="/dashboard" @mouseenter="prefetch">Dashboard</NuxtLink>
</template>
Edge Caching With Route Rules
Nuxt's route rules let you configure caching per route:
// nuxt.config.ts
routeRules: {
'/': { swr: 3600 },
'/blog/**': { swr: 86400 },
'/api/products': { cache: { maxAge: 300 } },
'/api/user/**': { cache: false },
}
The swr (stale-while-revalidate) value means users see cached content immediately, and the new version generates in the background. This is the pattern that makes the perceived performance of ISR match static generation.
Font Optimization
Web fonts are a common performance killer. @nuxtjs/google-fonts and @nuxt/fonts handle this correctly — they download fonts, serve them from your own domain, and use font-display: swap:
// nuxt.config.ts
fonts: {
families: [
{ name: 'Inter', weights: [400, 500, 600, 700] },
],
defaults: {
preload: true,
},
},
Preload only the font weights you actually use. Preloading unused weights is a net negative — it adds HTTP requests without improving any visible metric.
Measuring After Each Change
Performance optimization without measurement is guesswork. After each change, run a Lighthouse audit in an incognito window (to avoid extension interference) and record the scores. Track the Network tab payload sizes.
The changes that make the biggest difference in my experience, in rough order:
- Image optimization with correct sizing and modern formats (often 40-60% size reduction)
- Deferring third-party scripts to idle
- Lazy loading large below-the-fold components
- Removing unused JavaScript dependencies
- Font preloading with correct weights
- Edge caching for public content
None of these are magic tricks. They are disciplined application of known techniques. The results are real and measurable, and they compound — a site that does all of these well does not have a 95 Lighthouse score, it has a 98.
If you want a performance audit of your Nuxt application or help designing an optimization strategy, I can run through it methodically. Book a call: calendly.com/jamesrossjr.