1. Bundle Analysis
// nuxt.config.ts
export default defineNuxtConfig({
build: {
analyze: true, // Bật webpack-bundle-analyzer
},
vite: {
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-ui': ['@headlessui/vue', '@heroicons/vue'],
'vendor-charts': ['chart.js', 'vue-chartjs'],
},
},
},
},
},
})
# Phân tích bundle
npx nuxi analyze
# Kiểm tra bundle size
npx nuxi build --analyze
2. Lazy Loading Components
<script setup>
// Lazy load component — tự động code-split
const HeavyChart = defineAsyncComponent(() =>
import('~/components/HeavyChart.vue')
)
// Nuxt auto-import: prefix "Lazy"
// <LazyHeavyChart /> sẽ tự code-split
</script>
<template>
<div>
<!-- Chỉ load khi visible -->
<LazyHeavyChart v-if="showChart" />
<!-- Lazy load với Suspense -->
<Suspense>
<template #default>
<HeavyChart :data="chartData" />
</template>
<template #fallback>
<div class="skeleton h-64"></div>
</template>
</Suspense>
</div>
</template>
3. Image Optimization
<template>
<!-- Nuxt Image — tự động optimize -->
<NuxtImg
src="/images/hero.jpg"
width="800"
height="400"
format="webp"
quality="80"
loading="lazy"
placeholder
sizes="sm:100vw md:50vw lg:800px"
/>
<!-- NuxtPicture — responsive format -->
<NuxtPicture
src="/images/banner.jpg"
format="avif,webp"
:img-attrs="{ class: 'rounded-lg' }"
/>
</template>
4. Payload Optimization
// Giảm payload size khi SSR
export default defineNuxtConfig({
experimental: {
payloadExtraction: true, // Tách payload riêng file
},
})
// composables/useOptimizedFetch.ts
export function useOptimizedFetch<T>(url: string) {
return useFetch<T>(url, {
// Chỉ pick fields cần thiết
transform: (data: any[]) => data.map(({ id, title, slug }) => ({
id, title, slug,
})),
// Cache key để deduplicate
key: url,
})
}
5. Caching Strategies
// server/api/products.get.ts — Route caching
export default defineCachedEventHandler(async (event) => {
const products = await db.select().from(productsTable)
return products
}, {
maxAge: 60 * 10, // Cache 10 phút
staleMaxAge: 60 * 60, // Stale 1 giờ
swr: true, // Stale-while-revalidate
name: 'products-list',
getKey: (event) => getQuery(event).category as string || 'all',
})
// nuxt.config.ts — ISR
export default defineNuxtConfig({
routeRules: {
'/': { isr: 60 },
'/blog/**': { isr: 3600 },
'/api/**': { cors: true, headers: { 'cache-control': 'max-age=60' } },
'/admin/**': { ssr: false },
},
})
6. Core Web Vitals
| Metric | Good | Optimal technique |
|---|---|---|
| LCP | < 2.5s | Preload hero image, SSR, font-display: swap |
| INP | < 200ms | Debounce input, web workers, virtualize lists |
| CLS | < 0.1 | Set width/height on img, skeleton UI |
// plugins/web-vitals.client.ts
import { onCLS, onINP, onLCP } from 'web-vitals'
export default defineNuxtPlugin(() => {
onLCP(sendToAnalytics)
onINP(sendToAnalytics)
onCLS(sendToAnalytics)
})
function sendToAnalytics(metric: any) {
navigator.sendBeacon('/api/analytics/vitals', JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
}))
}
7. Virtual Scrolling
<script setup>
import { useVirtualList } from '@vueuse/core'
const allItems = ref(Array.from({ length: 10000 }, (_, i) => ({
id: i,
title: `Item ${i}`,
})))
const { list, containerProps, wrapperProps } = useVirtualList(allItems, {
itemHeight: 48,
overscan: 10,
})
</script>
<template>
<div v-bind="containerProps" class="h-96 overflow-auto">
<div v-bind="wrapperProps">
<div v-for="{ data } in list" :key="data.id" class="h-12 px-4 flex items-center">
{{ data.title }}
</div>
</div>
</div>
</template>
Next article: Deployment & Hosting — Vercel, Cloudflare, Docker, CI/CD.