Nuxt 3: SSR và sinh trang tĩnh, hướng dẫn đầy đủ
Làm chủ SSR và sinh trang tĩnh với Nuxt 3. Từ useFetch đến route rules, tìm hiểu cách tối ưu hiệu năng các ứng dụng Vue.js.

Nuxt 3 thay đổi cách xây dựng các ứng dụng Vue.js bằng việc cung cấp nhiều chế độ render phù hợp với các tình huống sử dụng khác nhau. Từ Server-Side Rendering (SSR) đến sinh trang tĩnh và render lai, framework mang lại tính linh hoạt đáng kể để tối ưu hiệu năng và SEO.
Hướng dẫn này giả định người đọc đã có kiến thức cơ bản về Vue 3 và Composition API. Việc quen thuộc với khái niệm render phía máy chủ sẽ hữu ích nhưng không bắt buộc, vì các kiến thức nền tảng được giải thích xuyên suốt bài.
Hiểu các chế độ render của Nuxt 3
Trước khi đi sâu vào code, điều thiết yếu là hiểu sự khác biệt giữa các chế độ render hiện có. Mỗi chế độ đáp ứng những nhu cầu cụ thể về hiệu năng, SEO và trải nghiệm người dùng.
SSR (Server-Side Rendering) sinh HTML trên máy chủ cho mỗi yêu cầu. Sinh trang tĩnh (SSG) tạo trước toàn bộ trang vào thời điểm build. Chế độ lai cho phép kết hợp các phương pháp này theo từng trang.
// Cấu hình các chế độ render khác nhau
export default defineNuxtConfig({
// SSR bật mặc định (khuyến nghị cho SEO)
ssr: true,
// Sinh trang tĩnh: render trước tất cả các trang
// Sử dụng 'npm run generate' để build
// target: 'static', // Cú pháp Nuxt 2
// Chế độ lai: cấu hình theo từng route
routeRules: {
// Trang chủ: render trước và đưa vào cache
'/': { prerender: true },
// Blog: sinh trang tĩnh
'/blog/**': { prerender: true },
// Dashboard: chỉ render phía client
'/dashboard/**': { ssr: false },
// API: không pre-render
'/api/**': { prerender: false }
}
})Cấu hình này thể hiện sức mạnh của chế độ lai: mỗi phần của ứng dụng dùng chế độ render phù hợp nhất với nhu cầu của mình.
Lấy dữ liệu với useFetch và useAsyncData
Nuxt 3 cung cấp hai composable chính để lấy dữ liệu theo cách isomorphic. Các composable này hoạt động cả phía máy chủ lẫn phía client, với việc quản lý hydrat hóa tự động.
useFetch là một wrapper quanh useAsyncData giúp đơn giản hóa các lời gọi HTTP. useAsyncData mang lại khả năng kiểm soát cao hơn trong các tình huống nâng cao.
<script setup lang="ts">
// pages/blog/[slug].vue
// Trang chi tiết bài viết với useFetch
// Lấy tham số route
const route = useRoute()
// useFetch: lấy dữ liệu tự động
// Dữ liệu được lấy ở phía máy chủ rồi hydrat hóa ở phía client
const { data: article, pending, error } = await useFetch(
`/api/articles/${route.params.slug}`,
{
// Khóa duy nhất cho cache và khử trùng lặp
key: `article-${route.params.slug}`,
// Biến đổi dữ liệu nếu cần
transform: (response) => response.data,
// Tùy chọn cache
getCachedData: (key) => {
// Kiểm tra dữ liệu có trong cache không
const nuxtApp = useNuxtApp()
return nuxtApp.payload.data[key]
}
}
)
// Xử lý lỗi với điều hướng
if (error.value) {
throw createError({
statusCode: 404,
message: 'Không tìm thấy bài viết'
})
}
</script>
<template>
<div>
<div v-if="pending" class="loading">
Đang tải bài viết...
</div>
<article v-else-if="article">
<h1>{{ article.title }}</h1>
<div v-html="article.content" />
</article>
</div>
</template>Với những tình huống cần kiểm soát nhiều hơn, useAsyncData cho phép thực thi bất kỳ hàm bất đồng bộ nào.
<script setup lang="ts">
// pages/products/index.vue
// Danh sách sản phẩm với useAsyncData và bộ lọc
const route = useRoute()
// useAsyncData: kiểm soát hoàn toàn logic fetching
const { data: products, refresh } = await useAsyncData(
'products-list',
async () => {
// Lấy từ nhiều nguồn nếu cần
const [productsResponse, categoriesResponse] = await Promise.all([
$fetch('/api/products', {
query: {
category: route.query.category,
sort: route.query.sort || 'date'
}
}),
$fetch('/api/categories')
])
// Kết hợp và biến đổi dữ liệu
return {
products: productsResponse.data,
categories: categoriesResponse.data,
total: productsResponse.meta.total
}
},
{
// Làm mới khi query params thay đổi
watch: [() => route.query]
}
)
// Hàm làm mới thủ công
const updateFilters = async (newCategory: string) => {
await navigateTo({
query: { ...route.query, category: newCategory }
})
}
</script>Các composable này tránh tình trạng fetch hai lần: dữ liệu lấy được ở máy chủ được tuần tự hóa vào payload HTML và tái sử dụng trong quá trình hydrat hóa ở phía client.
Tùy chỉnh SSR bằng server hooks
SSR của Nuxt 3 có thể được tùy chỉnh thông qua các server hook. Các hook này cho phép can thiệp vào nhiều giai đoạn của chu trình render để thay đổi hành vi mặc định.
// Plugin máy chủ để tùy chỉnh quá trình render SSR
export default defineNitroPlugin((nitroApp) => {
// Hook được thực thi trước khi render mỗi trang
nitroApp.hooks.hook('render:html', (html, { event }) => {
// Chèn script hoặc metadata
html.head.push(`
<script>
// Phân tích hoặc cấu hình toàn cục
window.__APP_CONFIG__ = {
environment: '${process.env.NODE_ENV}',
apiUrl: '${process.env.API_URL}'
}
</script>
`)
})
// Hook quản lý cache render
nitroApp.hooks.hook('render:response', (response, { event }) => {
// Thêm header cache tùy chỉnh
const path = event.path
if (path.startsWith('/blog/')) {
// Cache dài cho bài viết blog
response.headers['Cache-Control'] = 'public, max-age=3600, s-maxage=86400'
} else if (path.startsWith('/api/')) {
// Không cache cho API
response.headers['Cache-Control'] = 'no-store'
}
})
})Hook render:response lý tưởng để triển khai các chiến lược cache HTTP. Việc kết hợp SSR với một CDN tôn trọng các header Cache-Control cho phép phục vụ các trang đã render trước trong khi vẫn duy trì khả năng vô hiệu hóa cache.
Sinh trang tĩnh với nuxt generate
Sinh trang tĩnh xây dựng tất cả các trang trước thời điểm build. Cách tiếp cận này lý tưởng cho các website có nội dung ổn định như blog, tài liệu hoặc trang marketing.
Với các route động, Nuxt cần biết tất cả URL cần sinh. Hook prerender:routes cho phép định nghĩa các route này theo cách lập trình.
// Cấu hình đầy đủ cho sinh trang tĩnh
export default defineNuxtConfig({
// Bật sinh trang tĩnh
nitro: {
prerender: {
// Bật quét link tự động
crawlLinks: true,
// Các route luôn cần đưa vào
routes: ['/', '/about', '/contact'],
// Bỏ qua một số route
ignore: ['/admin', '/api']
}
},
hooks: {
// Hook để sinh các route động
async 'prerender:routes'(ctx) {
// Lấy bài viết từ API hoặc DB
const articles = await fetch('https://api.example.com/articles')
.then(res => res.json())
// Thêm các route bài viết
for (const article of articles) {
ctx.routes.add(`/blog/${article.slug}`)
}
// Lấy danh mục
const categories = await fetch('https://api.example.com/categories')
.then(res => res.json())
for (const category of categories) {
ctx.routes.add(`/category/${category.slug}`)
}
}
}
})Với các dự án có nhiều trang, crawler tự động có thể không đủ. Đây là cách tiếp cận chắc chắn hơn với một file cấu hình riêng.
// Tiện ích sinh danh sách route động
import { prisma } from './prisma'
export async function getAllStaticRoutes(): Promise<string[]> {
const routes: string[] = []
// Bài viết blog
const articles = await prisma.article.findMany({
where: { published: true },
select: { slug: true, category: { select: { slug: true } } }
})
for (const article of articles) {
routes.push(`/blog/${article.category.slug}/${article.slug}`)
}
// Trang sản phẩm
const products = await prisma.product.findMany({
where: { active: true },
select: { slug: true }
})
for (const product of products) {
routes.push(`/products/${product.slug}`)
}
// Trang tag
const tags = await prisma.tag.findMany({
select: { slug: true }
})
for (const tag of tags) {
routes.push(`/tags/${tag.slug}`)
}
return routes
}Sẵn sàng chinh phục phỏng vấn Vue.js / Nuxt.js?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Render lai với routeRules
Render lai là tính năng nổi bật của Nuxt 3. Nó cho phép định nghĩa các quy tắc render khác nhau cho từng route, kết hợp những điểm mạnh của SSR và SSG.
// Cấu hình nâng cao cho render lai
export default defineNuxtConfig({
routeRules: {
// Trang marketing: render trước và cache lâu dài
'/': { prerender: true },
'/pricing': { prerender: true },
'/features/**': { prerender: true },
// Blog: ISR (Incremental Static Regeneration)
// Tái xác thực mỗi giờ
'/blog/**': {
isr: 3600,
prerender: true
},
// Tài liệu: cache CDN với tái xác thực
'/docs/**': {
swr: 86400, // Stale-while-revalidate
prerender: true
},
// E-commerce: SSR với cache ngắn
'/products/**': {
ssr: true,
cache: {
maxAge: 60,
staleMaxAge: 300
}
},
// Giỏ hàng và thanh toán: chỉ phía client
'/cart': { ssr: false },
'/checkout/**': { ssr: false },
// Dashboard: chế độ SPA
'/dashboard/**': {
ssr: false,
// Tắt pre-render
prerender: false
},
// Route API: không cache mặc định
'/api/**': {
cors: true,
headers: {
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE'
}
}
}
})Cấu hình này minh họa kiến trúc điển hình của ứng dụng hiện đại: trang công khai được tối ưu cho SEO bằng SSG, trong khi các phần tương tác sử dụng render phía client.
Tối ưu hiệu năng với cache dữ liệu
Ngoài cache trang, Nuxt 3 còn cho phép cache dữ liệu đã lấy được. Chiến lược này giảm tải cho API và cải thiện thời gian phản hồi.
// Endpoint API có cache dữ liệu
import { getArticleBySlug } from '~/server/utils/articles'
export default defineCachedEventHandler(
async (event) => {
const slug = getRouterParam(event, 'slug')
if (!slug) {
throw createError({
statusCode: 400,
message: 'Thiếu slug'
})
}
const article = await getArticleBySlug(slug)
if (!article) {
throw createError({
statusCode: 404,
message: 'Không tìm thấy bài viết'
})
}
return article
},
{
// Khóa cache dựa trên slug
getKey: (event) => `article-${getRouterParam(event, 'slug')}`,
// Thời gian cache: 1 giờ
maxAge: 3600,
// Stale-while-revalidate: phục vụ cache cũ trong khi cập nhật
staleMaxAge: 7200,
// Vô hiệu hóa cache theo tag
tags: ['articles']
}
)Để vô hiệu hóa cache khi nội dung thay đổi, Nuxt cung cấp hệ thống tag.
// Cập nhật bài viết kèm vô hiệu hóa cache
import { updateArticle } from '~/server/utils/articles'
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug')
const body = await readBody(event)
// Cập nhật bài viết
const article = await updateArticle(slug, body)
// Vô hiệu hóa cache cho bài viết này
await useStorage('cache').removeItem(`nitro:handlers:article-${slug}`)
// Hoặc vô hiệu hóa theo tag (toàn bộ bài viết)
// await useStorage('cache').clear('articles')
return article
})Trong môi trường sản xuất với nhiều instance, cache trong bộ nhớ là không đủ. Khuyến nghị cấu hình Redis hoặc một hệ thống phân tán khác qua cấu hình Nitro để đảm bảo tính nhất quán giữa các instance.
Quản lý SEO và metadata
SSR cho phép tối ưu SEO bằng cách sinh metadata ở phía máy chủ. Nuxt 3 cung cấp nhiều cách để quản lý meta tag một cách động.
<script setup lang="ts">
// pages/blog/[slug].vue
// Trang blog với SEO được tối ưu
const route = useRoute()
const { data: article } = await useFetch(`/api/articles/${route.params.slug}`)
// Cấu hình SEO động dựa trên bài viết
useSeoMeta({
title: article.value?.title,
description: article.value?.excerpt,
ogTitle: article.value?.title,
ogDescription: article.value?.excerpt,
ogImage: article.value?.coverImage,
ogType: 'article',
twitterCard: 'summary_large_image',
twitterTitle: article.value?.title,
twitterDescription: article.value?.excerpt,
twitterImage: article.value?.coverImage
})
// Dữ liệu có cấu trúc cho Google
useHead({
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Article',
headline: article.value?.title,
description: article.value?.excerpt,
image: article.value?.coverImage,
datePublished: article.value?.publishedAt,
dateModified: article.value?.updatedAt,
author: {
'@type': 'Organization',
name: 'SharpSkill'
}
})
}
]
})
</script>Đối với trang tĩnh, metadata có thể được định nghĩa trực tiếp trong component.
<script setup lang="ts">
// pages/about.vue
// Trang tĩnh với SEO
definePageMeta({
title: 'Giới thiệu'
})
useSeoMeta({
title: 'Giới thiệu SharpSkill | Chuẩn bị phỏng vấn kỹ thuật',
description: 'Khám phá SharpSkill, nền tảng chuẩn bị phỏng vấn kỹ thuật. Sứ mệnh: giúp các nhà phát triển thành công trong các buổi phỏng vấn kỹ thuật.',
ogTitle: 'Giới thiệu SharpSkill',
ogDescription: 'Nền tảng chuẩn bị phỏng vấn kỹ thuật',
ogImage: '/images/og-about.webp'
})
</script>Triển khai và lưu ý ở môi trường sản xuất
Lựa chọn triển khai phụ thuộc vào chế độ render được dùng. Dưới đây là các tùy chọn chính và cấu hình của chúng.
// Cấu hình cho các môi trường triển khai khác nhau
export default defineNuxtConfig({
nitro: {
// Preset theo nền tảng đích
// preset: 'vercel', // Vercel
// preset: 'netlify', // Netlify
// preset: 'cloudflare-pages', // Cloudflare
// preset: 'node-server', // Node.js cổ điển
// Cấu hình cho Node.js trong môi trường sản xuất
preset: 'node-server',
// Nén phản hồi
compressPublicAssets: true,
// Cấu hình kho lưu trữ cache
storage: {
cache: {
driver: 'redis',
url: process.env.REDIS_URL
}
}
},
// Biến môi trường runtime
runtimeConfig: {
// Bí mật (không được lộ ra phía client)
apiSecret: process.env.API_SECRET,
// Cấu hình công khai
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
}
}
})Với triển khai tĩnh, lệnh npm run generate tạo ra thư mục .output/public sẵn sàng để triển khai trên bất kỳ host file tĩnh nào.
# Sinh trang tĩnh
npm run generate
# Nội dung của .output/public có thể được triển khai trên:
# - Vercel (tự động phát hiện)
# - Netlify (tự động cấu hình)
# - GitHub Pages
# - S3 + CloudFront
# - Bất kỳ CDN hoặc máy chủ file tĩnh nàoBắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Kết luận
Nuxt 3 mang lại tính linh hoạt vượt trội để render các ứng dụng Vue.js. Việc lựa chọn giữa SSR, SSG và render lai phụ thuộc vào nhu cầu cụ thể của từng dự án.
Điểm chính:
✅ SSR: lý tưởng cho nội dung động cần SEO tốt (e-commerce, trang tin tức)
✅ SSG: hoàn hảo cho nội dung ổn định (blog, tài liệu, trang marketing)
✅ Lai: phương án tốt nhất cho ứng dụng phức tạp với nhu cầu đa dạng
✅ useFetch/useAsyncData: hydrat hóa tự động và quản lý cache
✅ routeRules: cấu hình chi tiết cho hành vi của từng route
✅ Caching: nhiều chiến lược để tối ưu hiệu năng trong môi trường sản xuất
Việc kết hợp render lai với một chiến lược cache được cân nhắc kỹ giúp xây dựng các ứng dụng hiệu năng cao, được tối ưu cho SEO mà vẫn giữ được tính tương tác của Single Page Applications.
Thẻ
Chia sẻ
Bài viết liên quan

Câu hỏi phỏng vấn Vue.js: 25 câu để chinh phục công việc
Chuẩn bị phỏng vấn Vue.js với 25 câu hỏi cốt lõi. Từ reactivity đến composables, làm chủ các khái niệm quan trọng cho buổi phỏng vấn tới.

Vue 3 Pinia vs Vuex: So Sanh State Management va Cau Hoi Phong Van 2026
Phan tich chi tiet Pinia vs Vuex: kien truc, TypeScript, Composition API, hieu suat, chien luoc migration va cau hoi phong van Vue state management 2026.

Vue 3 Composition API: Hướng Dẫn Đầy Đủ Để Làm Chủ Reactivity
Làm chủ Vue 3 Composition API qua hướng dẫn thực hành này. Tìm hiểu ref, reactive, computed, watch và composables để xây dựng ứng dụng Vue hiệu suất cao.