---
title: "Nuxt Performance Guide: Optimizing Core Web Vitals and Lighthouse"
description: "How to achieve perfect Lighthouse scores with Nuxt 3 and 4. Strategies for Core Web Vitals, Nuxt Image, Lazy Hydration, and more. Optimize now!"
canonical_url: "https://www.blueshoe.io/blog/nuxt-lighthouse-performance/"
---

> Index of every page on this site: [llms.txt](https://www.blueshoe.io/llms.txt)

Performance is now a real ranking factor—not just a "nice to have" anymore. Nuxt (3 and 4) comes with a solid foundation, but getting a perfect Lighthouse score of 100/100 takes more than the default configuration. In this article, you'll learn strategies that actually work and how to optimize any Nuxt app.

![Nuxt Performance Optimization](https://www.blueshoe.io/img/blogs/vuejsnuxt.svg)

<global-blog-level-info>

- Basics in [Nuxt 3/4](https://nuxt.com/docs)
- [Vue.js](https://vuejs.org/) fundamentals
- [Core Web Vitals](https://web.dev/articles/vitals) and [Lighthouse](https://developer.chrome.com/docs/lighthouse/)
- Context on rendering modes: [Nuxt Generate vs. Server](https://www.blueshoe.io/blog/nuxt-generate-vs-server/)

If you have questions or need clarification, feel free to use the comment section below the article.

</global-blog-level-info>

We'll look at how Hybrid Rendering works, how Nuxt Image and Nuxt Scripts improve performance, and how Lazy Hydration helps reduce [Total Blocking Time (TBT)](https://developer.chrome.com/docs/lighthouse/performance/lighthouse-total-blocking-time). These tips work for both e-commerce platforms and content-heavy marketing websites.

## Why Lighthouse Matters for Nuxt Apps

If you've ever built a Nuxt app, you know the problem: The app feels fast, but Lighthouse still doesn't show the scores you want. That's because SPAs and hydrated apps often struggle with initial load metrics—even though Nuxt 3 and 4 support SSR and SSG.

Google now uses Core Web Vitals as a ranking signal. That means poor performance metrics can directly impact your visibility in search results. And that affects not just SEO, but also conversion rates.

The trick is to focus not just on making the app feel fast, but on making it measure fast. With the right strategies, a perfect Lighthouse score of 100/100 is achievable with Nuxt.

## The Right Rendering Mode Makes All the Difference

Before we dive into the details, let's quickly clarify which rendering modes exist and when each makes sense. With SSR (`ssr: true`), pages are rendered on the server on each request—good for dynamic content, but Time to First Byte (TTFB) suffers. SSG (`ssr: false` with `nitro.prerender`) generates pages at build time—extremely fast, but no dynamic content. And CSR (`ssr: false`) runs entirely in the browser—fast, but bad for SEO.

The real game changer is Hybrid Rendering. Nuxt 3 and 4 let you use different rendering strategies for different routes. This is the key to optimal performance:

```typescript
// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // Static marketing pages: Instantly available
    '/': { prerender: true },
    '/about': { prerender: true },
    '/contact': { prerender: true },
    
    // Dynamic content: SWR for fresh data with fast TTFB
    '/blog/**': { swr: 3600 }, // 1 hour cache
    '/products/**': { swr: 1800 }, // 30 minutes cache
    
    // Real-time data: Always fresh
    '/dashboard/**': { ssr: true },
    
    // Client-only: For interactive components
    '/admin/**': { ssr: false }
  }
})
```

The strategy is simple: Use SWR (Stale-While-Revalidate) for dynamic content like blog posts or product pages. This ensures fresh data while maintaining fast TTFB. For static marketing pages like "About" or "Contact", use prerendering—they'll be instantly available.

## Optimizing LCP: The Largest Visible Element Counts

[LCP (Largest Contentful Paint)](https://developer.chrome.com/docs/lighthouse/performance/lighthouse-largest-contentful-paint) measures when the largest visible element in the viewport is loaded. Usually, that's a large hero image or a render-blocking element. If that takes too long, your Lighthouse score suffers.

### Nuxt Image Makes the Difference

The [@nuxt/image](https://image.nuxt.com/) module should be in every optimized Nuxt app. It makes image optimization almost automatic:

```bash
npm install @nuxt/image
```

```typescript
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/image'],
  image: {
    format: ['webp', 'avif'],
    quality: 80,
    screens: {
      xs: 320,
      sm: 640,
      md: 768,
      lg: 1024,
      xl: 1280,
      xxl: 1536,
    },
  }
})
```

**Here's how to use it in components**:

```vue
<template>
  <NuxtImg
    src="/hero-image.jpg"
    alt="Hero Image"
    :sizes="{ sm: '100vw', md: '50vw', lg: '33vw' }"
    loading="eager"
    fetchpriority="high"
    preload
  />
</template>
```

Important: For your LCP element, you should use `preload` and `fetchpriority="high"`. The `sizes` prop ensures mobile users don't load the desktop version. And automatic format conversion to WebP or AVIF saves a lot of data.

### Loading Fonts the Right Way

Web fonts can really hurt your LCP if they're not optimized. That's where `@nuxt/fonts` helps:

```bash
npm install @nuxt/fonts
```

```typescript
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/fonts'],
  fonts: {
    families: [
      { name: 'Inter', provider: 'google' }
    ]
  }
})
```

**Implementation**:

```vue
<style>
@font-face {
  font-family: 'Inter';
  font-display: swap; /* Prevents invisible text during loading */
}
</style>
```

With `font-display: swap`, text is displayed immediately with a fallback font while the web font loads in the background. No more invisible text blocks.

## Eliminating CLS: No More Layout Jumps

[Cumulative Layout Shift (CLS)](https://web.dev/articles/cls) is every frontend developer's nightmare. Nothing is more annoying than pages that jump around while loading. The most common culprits are images without dimensions, late-loading ads, web fonts without `font-display: swap`, and dynamically injected content.

With Nuxt, you can get this under control:

**Enforce aspect ratios for all media**:

```vue
<template>
  <NuxtImg
    src="/product.jpg"
    alt="Product"
    :width="800"
    :height="600"
    loading="lazy"
  />
</template>
```

**Placeholders for dynamic content**:

```vue
<template>
  <div class="product-card">
    <!-- Placeholder during loading -->
    <div v-if="loading" class="skeleton">
      <div class="skeleton-image"></div>
      <div class="skeleton-text"></div>
    </div>
    
    <!-- Real content -->
    <ClientOnly>
      <ProductDetails :product="product" />
      <template #fallback>
        <div class="skeleton">Loading...</div>
      </template>
    </ClientOnly>
  </div>
</template>
```

**Reserved space for dynamic components**:

```vue
<template>
  <!-- Fixed container prevents layout shift -->
  <div class="ad-container" style="min-height: 250px;">
    <ClientOnly>
      <AdBanner />
    </ClientOnly>
  </div>
</template>
```

## Reducing TBT and INP: Don't Let JavaScript Block

When too much JavaScript runs on the main thread, it blocks interactivity. [TBT (Total Blocking Time)](https://developer.chrome.com/docs/lighthouse/performance/lighthouse-total-blocking-time) measures how long the main thread is blocked, [INP (Interaction to Next Paint)](https://web.dev/articles/inp) measures response time to clicks and other interactions.

### Loading Third-Party Scripts Smartly

With [@nuxt/scripts](https://scripts.nuxt.com/), you load third-party scripts like Google Tag Manager or Google Analytics with good load behavior and without blocking rendering. The official docs distinguish between **Registry Scripts** (pre-built integrations) and **Global Scripts** (arbitrary script URLs).

**Installation:**

```bash
npm install @nuxt/scripts
```

**Registry Scripts**—for supported services like GTM or Google Analytics, providing the ID is enough. By default, scripts load when Nuxt has finished hydrating (`onNuxtReady`), so they don't block the first paint:

```typescript
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/scripts'],
  scripts: {
    registry: {
      googleTagManager: { id: 'GTM-XXXXXXX' },
      googleAnalytics: { id: 'G-XXXXXXXXXX' },
    },
  },
})
```

**Global Scripts**—for arbitrary script URLs (e.g. your own analytics or tracking), use `globals`. With the array form you can pass [Script Options](https://scripts.nuxt.com/docs/api/use-script#nuxtusescriptoptions) like `trigger`:

```typescript
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/scripts'],
  scripts: {
    globals: {
      // URL only—loads with default options (e.g. onNuxtReady)
      myTracker: 'https://analytics.example.com/tracker.js',
      // With options: e.g. load only after hydration
      myScript: [
        { src: 'https://example.com/script.js' },
        { trigger: 'client' },
      ],
    },
  },
})
```

You can access globally registered scripts via `useNuxtApp().$scripts` (e.g. `$scripts.myTracker`). That way, third-party scripts stay under your control and don't block the main thread during initial rendering.

### Only Load Components When They're Needed

Nuxt makes lazy loading super easy. With the `Lazy` prefix, components are only loaded when they're actually needed:

```vue
<template>
  <!-- Only loaded when needed -->
  <LazyModal v-if="showModal" />
  <LazyChart v-if="showChart" />
</template>
```

For heavy libraries like Chart.js, you can use dynamic imports:

```vue
<script setup>
const loadChart = async () => {
  // Only imported when user scrolls to chart
  const { Chart } = await import('chart.js')
  // Initialize chart...
}
</script>

<template>
  <div @intersect="loadChart">
    <ChartComponent />
  </div>
</template>
```

### Lazy Hydration: The Advanced Trick

Lazy hydration is a real game changer. Instead of hydrating all components immediately, it only happens when they're actually needed:

```bash
npm install nuxt-delay-hydration
```

```typescript
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-delay-hydration'],
  delayHydration: {
    mode: 'mount',
    idleTimeout: 5000, // Hydration after 5 seconds of idle time
    replayClick: true
  }
})
```

**You can implement lazy hydration manually like this**:

```vue
<script setup>
import { useIntersectionObserver } from '@vueuse/core'

const target = ref(null)
const shouldHydrate = ref(false)

useIntersectionObserver(
  target,
  ([{ isIntersecting }]) => {
    if (isIntersecting) {
      shouldHydrate.value = true
    }
  }
)
</script>

<template>
  <div ref="target">
    <ClientOnly v-if="shouldHydrate">
      <HeavyComponent />
    </ClientOnly>
    <div v-else class="placeholder">Scroll for more...</div>
  </div>
</template>
```

The result: Hydration only happens when the component scrolls into the viewport. This significantly reduces TBT and makes your app much faster.

## SEO: The Technical Foundation

For SEO, [@nuxtjs/seo](https://nuxtseo.com/) is a great fit. The module is a real "set and forget" solution—configure it once, and it handles most SEO best practices:

```bash
npm install @nuxtjs/seo
```

```typescript
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/seo'],
  site: {
    url: 'https://www.your-domain.com',
    name: 'Your Project',
    description: 'Your performance-optimized website',
  },
  sitemaps: {
    hostname: 'https://www.your-domain.com',
  }
})
```

The module automatically generates sitemaps and robots.txt, makes Schema.org structured data super easy, and manages meta tags for social sharing. With `useSeoMeta` and `useSchemaOrg`, you have everything you need.

**Here's how to use it in components**:

```vue
<script setup>
useSeoMeta({
  title: 'Product Page',
  description: 'Product description',
  ogImage: '/product-image.jpg',
  twitterCard: 'summary_large_image'
})

useSchemaOrg([
  {
    '@type': 'Product',
    name: 'Product Name',
    description: 'Product description',
    image: '/product-image.jpg',
    offers: {
      '@type': 'Offer',
      price: '99.99',
      priceCurrency: 'USD'
    }
  }
])
</script>
```

## Continuously Monitoring Performance

### Unlighthouse: Test All Pages, Not Just the Homepage

[Unlighthouse](https://unlighthouse.dev/) is a recommended tool for performance testing. It scans every page of your website, not just the homepage:

```bash
npm install -D unlighthouse
```

```typescript
// unlighthouse.config.ts
export default {
  site: 'https://www.your-domain.com',
  urls: [
    '/',
    '/blog',
    '/products',
    // ... all important pages
  ],
  lighthouseOptions: {
    onlyCategories: ['performance', 'seo', 'accessibility']
  }
}
```

**Here's how to run it**:

```bash
npx unlighthouse --site https://www.your-domain.com
```

### CI/CD Integration

Integrate Lighthouse checks into your pull request workflow:

```yaml
# .github/workflows/lighthouse.yml
name: Lighthouse CI

on:
  pull_request:
    branches: [main]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: npm ci
      - run: npm run build
      - run: npm run preview &
      - uses: treosh/lighthouse-ci-action@v9
        with:
          urls: |
            http://localhost:3000
            http://localhost:3000/blog
          uploadArtifacts: true
          temporaryPublicStorage: true
```

The nice thing about it: You automatically detect performance regressions, continuously monitor Core Web Vitals, and find problems before they land in production.

## Quick Check: Do You Have Everything?

Before you start, here's a quick check of the most important points: Nuxt Image installed? LCP image marked with `preload` and `fetchpriority="high"`? Fonts optimized? Aspect ratios defined? Hybrid Rendering configured? Third-party scripts loaded with Nuxt Scripts? Lazy loading and lazy hydration active? SEO module installed? Unlighthouse set up for monitoring?

If you can check all these points, you're on a very good path to perfect Lighthouse scores.

## Conclusion

Performance optimization isn't a one-time fix, but an ongoing process. With Nuxt 3 and 4, you have all the tools you need to achieve perfect Lighthouse scores. The combination of Hybrid Rendering, optimized assets, and intelligent hydration strategy leads to measurably better Core Web Vitals.

My tip: Start with a Lighthouse audit of your current app, identify the biggest bottlenecks, and then implement one optimization at a time. Even small improvements can have a big impact.

## Sitemap

See the full [sitemap](https://www.blueshoe.io/sitemap.md) for all pages.
