Performance Optimization
MegaShop is now live, but its Lighthouse score is only 45—it takes 4.5 seconds to load the first screen, the images aren't optimized, and the JS bundle is too large. Charlie needs a Lighthouse score of 90+ and an LCP of less than 2 seconds for the first screen to stay competitive.
1. What You'll Learn
- Code Splitting and Tree Shaking: Vite Build Optimization and Bundle Analysis
- Image optimization: @nuxt/image + WebP + lazy loading + responsive srcset
- Component Lazy Loading and Asynchronous Processing: "Lazy" Prefix + Suspense
- Caching Strategy: HTTP Cache Headers + Nitro KV + CDN
- MegaShop Lighthouse 90+ Optimization in Practice
2. A True Story of an Architect
(1) Pain Point: Lighthouse score of 45, 4.5 seconds to first content paint
Alice opened MegaShop on her phone and it took 4.5 seconds for the products to appear. Charlie ran a Lighthouse audit and found that the JS bundle was 2.8 MB, all images were uncompressed PNGs, there were no cache headers, and the code for 50 components loaded on the first screen.
(2) Solutions for Nuxt 3 Performance Optimization
Nuxt 3 includes a variety of built-in performance optimization features:
// Image optimization with @nuxt/image
<NuxtImg src="/products/123.webp" width="400" height="400" loading="lazy" />
(3) Performance: Lighthouse score of 92, LCP of 1.3 seconds
After optimization, the JS bundle size was reduced to 180 KB (gzip), images are automatically served as WebP and are responsive, LCP for the first screen is 1.3 seconds, and the Lighthouse score is 92.
3. Code Splitting and Tree Shaking
(1) Performance Optimization Decision Tree
flowchart TB
A[Performance Issue] --> B{What type?}
B -->|Slow first paint| C[Code Splitting]
B -->|Large images| D[Image Optimization]
B -->|Slow subsequent loads| E[Caching Strategy]
B -->|JS too large| F[Tree Shaking]
C --> C1[Lazy components]
C --> C2[Dynamic imports]
D --> D1[@nuxt/image module]
D --> D2[WebP conversion]
E --> E1[HTTP Cache Headers]
E --> E2[Nitro KV Cache]
F --> F1[Check bundle with rollup-plugin-visualizer]
(2) Bundle Analysis
npm install -D rollup-plugin-visualizer
// nuxt.config.ts
import { visualizer } from 'rollup-plugin-visualizer'
export default defineNuxtConfig({
vite: {
plugins: [
visualizer({ filename: 'bundle-stats.html', open: true })
],
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor': ['vue', 'vue-router'],
'pinia': ['pinia']
}
}
}
}
}
})
(1) ▶ Example: Lazy Component Code Splitting
<template>
<div>
<!-- Core: loaded immediately -->
<AppHeader />
<ProductHero :product="featured" />
<!-- Below fold: lazy loaded -->
<LazyProductGrid :products="all" />
<LazyProductReviews :product-id="id" />
<!-- Conditional: loaded only when visible -->
<LazyNewsletterSignup v-if="showNewsletter" />
</div>
</template>
Output:
// Execution Successful
(3) Before-and-After Comparison of the Optimization
| Metric | Before Optimization | After Optimization | Improvement |
|---|---|---|---|
| JS Bundle (gzip) | 2.8 MB | 180 KB | -94% |
| Number of components on the first screen | 50 | 8 | -84% |
| JS Download on First Screen | 2.8 MB | 85 KB | -97% |
| LCP | 4.5s | 1.3s | -71% |
4. Image Optimization
(1) ▶ Example: Installing @nuxt/image
npm install @nuxt/image
Output:
# Command executed successfully
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/image'],
image: {
quality: 80,
format: ['webp', 'avif'],
screens: {
xs: 320, sm: 640, md: 768, lg: 1024, xl: 1280, xxl: 1536
}
}
})
(2) ▶ Example: NuxtImg Reactive Images
<template>
<div>
<!-- Auto WebP/AVIF + responsive srcset -->
<NuxtImg
src="/images/product-123.jpg"
width="800"
height="600"
format="webp"
loading="lazy"
sizes="sm:100vw md:50vw lg:33vw"
:modifiers="{ quality: 80 }"
alt="Premium Headphones"
/>
<!-- Placeholder with blur-up -->
<NuxtImg
src="/images/hero.jpg"
placeholder
width="1920"
height="1080"
format="webp"
alt="MegaShop Hero"
/>
</div>
</template>
Output:
// Execution Successful
(3) ▶ Example: NuxtPicture Multi-Format
<template>
<!-- NuxtPicture: auto-select best format per browser -->
<NuxtPicture
src="/images/banner.jpg"
width="1200"
height="400"
format="avif,webp"
loading="lazy"
alt="Sale Banner"
/>
</template>
Output:
// Execution Successful
(1) Comparison of Image Formats
| Format | Compression Ratio | Browser Support | Use Cases |
|---|---|---|---|
| AVIF | 🟢 Best (50% vs. JPG) | Chrome/Edge/Firefox | Modern browsers |
| WebP | 🟢 High (30% vs. JPG) | All major browsers | General-purpose preferred format |
| JPEG | 🟡 Benchmark | All | Compatibility Fallback |
| PNG | 🔴 Large | All | Transparent Images |
5. Caching Strategy
(1) ▶ Example: HTTP Cache Headers
// nuxt.config.ts - Cache headers per route
export default defineNuxtConfig({
routeRules: {
// Static assets: 1 year immutable cache
'/_nuxt/**': {
headers: { 'cache-control': 'public, max-age=31536000, immutable' }
},
// Images: 1 day cache
'/images/**': {
headers: { 'cache-control': 'public, max-age=86400, stale-while-revalidate=604800' }
},
// Product pages: ISR cache
'/products/**': {
swr: 86400,
headers: { 'cache-control': 'public, s-maxage=86400, stale-while-revalidate=3600' }
}
}
})
Output:
// Execution Successful
(2) ▶ Example: Nitro KV Cache API
// server/api/products/[id].get.ts - Cached handler
export default cachedEventHandler(
async (event) => {
const id = getRouterParam(event, 'id')
const product = await fetchProductFromDB(Number(id))
return product
},
{
maxAge: 60 * 60, // Cache for 1 hour
swr: true, // Serve stale while revalidating
getKey: (event) => `product:${getRouterParam(event, 'id')}`,
varies: ['Accept-Language'] // Different cache per language
}
)
Output:
// Execution Successful
(1) Comparison of Cache Hierarchies
| Level | Location | Speed | Control Method |
|---|---|---|---|
| Browser Cache | User Device | ⚡⚡⚡ Fastest | Cache-Control Header |
| CDN Edge Caching | Nearest Node | ⚡⚡ Fast | CDN Configuration + Vary |
| Nitro KV Cache | Server Memory | ⚡ Fast | cachedEventHandler |
| ISR Page Cache | Nitro Storage | ⚡ Fast | routeRules swr |
6. Comprehensive Example: MegaShop Performance Optimization Configuration
// nuxt.config.ts - Full performance optimization
export default defineNuxtConfig({
ssr: true,
modules: ['@nuxt/image', '@nuxtjs/tailwindcss'],
// Image optimization
image: {
quality: 80,
format: ['webp', 'avif'],
screens: { xs: 320, sm: 640, md: 768, lg: 1024, xl: 1280 }
},
// Vite build optimization
vite: {
build: {
cssCodeSplit: true,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('pinia')) return 'vendor-pinia'
if (id.includes('vue')) return 'vendor-vue'
return 'vendor-libs'
}
}
}
}
}
},
// Route-level caching strategy
routeRules: {
'/': { prerender: true },
'/products': { swr: 3600 },
'/products/**': { swr: 86400 },
'/admin/**': { ssr: false },
'/_nuxt/**': { headers: { 'cache-control': 'public, max-age=31536000, immutable' } },
'/images/**': { headers: { 'cache-control': 'public, max-age=86400' } }
},
// App performance hints
experimental: {
payloadExtraction: true // Reduce payload size
}
})
<!-- pages/index.vue - Optimized homepage -->
<template>
<div>
<AppHeader />
<!-- Above fold: eager load -->
<section class="hero">
<NuxtImg src="/images/hero.jpg" width="1200" height="400" format="webp"
priority alt="MegaShop" />
<h1>MegaShop</h1>
</section>
<!-- Below fold: lazy load -->
<LazyProductGrid :products="featured" />
<!-- Conditional: only load when in viewport -->
<LazyNewsletterSignup v-if="showNewsletter" />
</div>
</template>
<script setup lang="ts">
const { data: featured } = await useFetch('/api/products/featured')
const showNewsletter = ref(false)
</script>
❓ FAQ
image.domains to add external domains: image: { domains: ['cdn.example.com'] }. NuxtImg will automatically optimize external images.img tag?srcset, converts images to WebP/AVIF, crops them to fit the device's dimensions, and enables lazy loading. With a regular img tag, you need to handle all these optimizations manually.manualChunks?manualChunks should only be used to split third-party libraries.📖 Summary
- Code splitting: Lazy components + dynamic import + manualChunks; only the necessary code is loaded for the first screen
- Image optimization: @nuxt/image—automatic WebP/AVIF, responsive srcset, and lazy loading
- Caching strategy: Browser → CDN → Nitro ISR → dynamic rendering, multi-tier caching
- Use
rollup-plugin-visualizerfor bundle analysis to identify size bottlenecks - MegaShop after optimization: JS 180 KB (gzip) + LCP 1.3 s + Lighthouse score of 92
📝 Exercises
- Basic Exercise (Difficulty: ⭐): Install @nuxt/image, replace all
imgtags withNuxtImg, and verify the automatic WebP conversion. - Advanced Exercise (Difficulty: ⭐⭐): Use
rollup-plugin-visualizerto analyze the bundle, identify the largest dependency, and configuremanualChunksto split it. - Challenge (Difficulty: ⭐⭐⭐): Achieve a Lighthouse score of 90+: Image optimization + code splitting + caching strategies, using Lighthouse CI for automated audits
---|



