Intermediate

Table of Contents

  • Why Lighthouse Matters for Nuxt Apps
  • The Right Rendering Mode Makes All the Difference
  • Optimizing LCP: The Largest Visible Element Counts
  • Eliminating CLS: No More Layout Jumps
  • Reducing TBT and INP: Don't Let JavaScript Block
  • SEO: The Technical Foundation
  • Continuously Monitoring Performance
  • Quick Check: Do You Have Everything?
  • Conclusion

Meet the Author

Last updated:2026-09-28

NuxtVue.jsPerformanceSEODevelopment

2026-09-28

Performance Optimization for NuxtNuxt Performance Guide: Optimizing Core Web Vitals and Lighthouse

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

Prerequisites

You should have the following knowledge to use the article optimally:

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

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). 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:

// 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) 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 module should be in every optimized Nuxt app. It makes image optimization almost automatic:

npm install @nuxt/image
// 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:

<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:

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

Implementation:

<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) 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:

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

Placeholders for dynamic content:

<template>
  <div class="product-card">
    
    <div v-if="loading" class="skeleton">
      <div class="skeleton-image">div>
      <div class="skeleton-text">div>
    div>
    
    
    <ClientOnly>
      <ProductDetails :product="product" />
      <template #fallback>
        <div class="skeleton">Loading...div>
      template>
    ClientOnly>
  div>
template>

Reserved space for dynamic components:

<template>
  
  <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) measures how long the main thread is blocked, INP (Interaction to Next Paint) measures response time to clicks and other interactions.

Loading Third-Party Scripts Smartly

With @nuxt/scripts, 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:

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:

// 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 like trigger:

// 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:

<template>
  
  <LazyModal v-if="showModal" />
  <LazyChart v-if="showChart" />
template>

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

<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:

npm install nuxt-delay-hydration
// 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:

<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 is a great fit. The module is a real "set and forget" solution—configure it once, and it handles most SEO best practices:

npm install @nuxtjs/seo
// 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:

<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 is a recommended tool for performance testing. It scans every page of your website, not just the homepage:

npm install -D unlighthouse
// 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:

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

CI/CD Integration

Integrate Lighthouse checks into your pull request workflow:

# .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.


Do you have questions or an opinion? With your GitHub account you can let us know...


These companies trust Team Blueshoe

  • Allgau
  • Allianz
  • Audi
  • Chip
  • Deutsches Museum
  • DieProduktMacher
  • Dom Perignon
  • Finanz
  • Haufe Lexware
  • Kaufland
  • Luma
  • Ofa
  • SWM
  • Travian
  • TUHH
  • Uni Augsburg
  • Winter Company